Repository navigation
Guard and retry the release repo fetches, drop a dead script - #618
Merged
Merged
Conversation
A transient GitHub rate limit could fail a config build outright, with an error naming neither the status code nor the URL it came from. render-images.py and set-versions.py fetch base.yml and images.yml from raw.githubusercontent.com and fed the response body straight to yaml.full_load without ever checking the status, so an HTTP error page was parsed as though it were the versions file and the build then died downstream on the resulting garbage. Both scripts now check the status and retry transient failures: a rate limit that clears on its own no longer breaks the build at all, and one that persists reports its status and the full URL. The limit is easy to trip. raw.githubusercontent.com rate-limits per IP, and every config build fetches these URLs unauthenticated, so a handful of builds in an afternoon will do it. What the operator saw depended on whether the error body happened to parse as YAML: GitHub's 199-byte 429 body produces a composer error about the response body, while an HTML error page parses as a bare string and fails further down with a TypeError once it is indexed like a dict. Neither mentions rate limiting, and neither names the URL, so the traceback points nowhere near the cause. Both scripts now fetch through a fetch_yaml() helper: a requests.Session whose adapter carries urllib3 Retry with total=4, backoff_factor=1.5 and a status_forcelist of 429, 500, 502, 503 and 504, followed by raise_for_status(). Retry honours a Retry-After header by default, so a server that says how long to wait is obeyed. No call site in this repo checked a status code before this change; grep found no raise_for_status anywhere in it. raise_on_status is off deliberately. With it on, an exhausted status_forcelist retry raises urllib3's RetryError, which reports "too many 429 error responses" against a connection pool and drops the URL. With it off the final response comes back and raise_for_status() reports the status and the full URL, the same shape a status that is never retried produces. One error shape for every failure, and it always names the URL. The helper is duplicated in each script rather than shared. gilt.yml copies each one alone into the operator's configuration repo, runs it there, and rm's it afterwards, so no importable sibling module exists at the point of use. The adapter is mounted for http:// as well as https:// because VERSIONS_URL and IMAGES_URL are environment-overridable and an http:// override would otherwise silently skip the retries. Verified against a local server reproducing the pattern: 429, 429, 200 recovers in 3.0s and returns parsed YAML; a persistent 429 raises "429 Client Error: Too Many Requests for url: ..." after 21s of backoff; a 404 fails immediately without pointless retries. Worst case therefore adds about 21s per URL, and render-images.py fetches two. Assisted-by: Claude:claude-opus-5 Signed-off-by: Roger Luethi <luethi@osism.tech>
The script rendered environments/manager/requirements.yml from a template, filling in the ansible_roles versions it fetched from <manager_version>/base.yml in the release repository. That output file no longer exists. #362 removed environments/manager/requirements.yml outright and moved the role and collection versions into environments/manager/run.sh, set through ANSIBLE_COLLECTION_SERVICES_VERSION and ANSIBLE_PLAYBOOKS_MANAGER_VERSION so the seed process pins them exactly the way the update process later does. #365 then dropped the script from gilt.yml, leaving nothing that invokes it: not gilt.yml, Makefile, tox.ini, .zuul.yaml, environments/ or inventory/, and no other OSISM repository refers to this copy. It has been dead for close to two years, and it did not look dead because it kept being maintained. #382, #383 and #384 swept changes across the render scripts seven weeks after the unwiring and picked this one up along the way, which is how it acquired the unguarded requests.get that the previous commit had to reason about in the two scripts that are actually reachable. The functionality survives elsewhere, reimplemented: the osism-ansible container image carries its own render-ansible-requirements.py under files/src. That copy reads base.yml from a git checkout of the release repository made during the image build and imports no HTTP client at all, so it never had the failure mode this repo's copy did. Assisted-by: Claude:claude-opus-5 Signed-off-by: Roger Luethi <luethi@osism.tech>
ideaship
marked this pull request as ready for review
September 9, 2026 13:33
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two scripts are fixed and one is deleted.
render-images.pyandset-versions.pyare made to check the HTTP status of the files they fetchfrom the release repository;
render-ansible-requirements.pyheld a third copyof the same bug but has been dead since 2023, so it is removed rather than
fixed.
A 429 from
raw.githubusercontent.comwas parsed as if it were the versionsfile. Every config build fetches
base.ymlandimages.ymlunauthenticated,raw.githubusercontent.com rate-limits per IP, and neither
render-images.pynor
set-versions.pylooked at the status code before handingr.texttoyaml.full_load— so a rate limit that clears in a minute failed the buildwith a traceback naming neither the status nor the URL.
The symptom is not even stable: it depends on whether the error body happens to
parse as YAML. GitHub's 199-byte 429 body gives a
yamlcomposer error aboutthe response body; an HTML error page parses as a bare string and fails further
down with a
TypeErroronce it is indexed like a dict.grep -rn raise_for_status src/returned nothing — no call site in this repo checked astatus.
What is here
Two commits, reviewable independently:
fetch_yaml()helper —requests.Sessionwhose adapter carries urllib3Retry(total=4,backoff_factor=1.5,status_forcelist429/500/502/503/504) followed by
raise_for_status(). A self-clearing rate limit is nowwaited out instead of failing the build; a persistent one reports its status
and full URL.
render-ansible-requirements.py. Dead since 2023 andunrelated to the fix except that it held a fourth copy of the same unguarded
requests.get— see below.Two decisions worth a reviewer's attention
The helper is duplicated, not shared.
gilt.ymlcopies each script aloneinto the operator's configuration repo, runs it there, and
rms it —render-images.pyintoenvironments/manager/,set-versions.pyintoenvironments/. There is no importable sibling module at the point of use, soa single shared helper is not reachable. ~20 lines are duplicated deliberately.
raise_on_status=Falseis deliberate. With urllib3's default, an exhaustedstatus_forcelistretry raisesRetryError— "too many 429 error responses"against a connection pool, no URL. With it off, the final response comes back
and
raise_for_status()names the status and the full URL, the same shape anever-retried status produces. One error shape for every failure, and it always
names the URL.
The adapter is also mounted for
http://, becauseVERSIONS_URLandIMAGES_URLare environment-overridable and anhttp://override wouldotherwise silently skip the retries.
Why the second commit deletes a script rather than guarding it
render-ansible-requirements.pyrenderedenvironments/manager/requirements.yml. That output file no longer exists:into
environments/manager/run.shgilt.ymlNothing has invoked it since — not
gilt.yml,Makefile,tox.ini,.zuul.yaml,environments/orinventory/, and no other OSISM repositoryrefers to this copy. It did not look dead because it kept being maintained:
#382, #383 and #384 swept
changes across the render scripts seven weeks after the unwiring and picked it
up along the way, which is how it acquired the unguarded
requests.getin thefirst place.
The functionality survives, reimplemented: the osism-ansible container image
carries its own
render-ansible-requirements.pyunderfiles/src, which readsbase.ymlfrom a git checkout of osism/release made during the image build andimports no HTTP client at all. It never had this failure mode.
Verification
Against a local server reproducing the 429 pattern:
429, 429, 200429429 Client Error: Too Many Requests for url: ...after 21s of backoff404Worst case adds ~21s per URL, and
render-images.pyfetches two.Against live GitHub, running what
generics-tox-test-*runs(
python3 src/render-images.py) for all three pinned versions —latest,9.1.0,9.0.0: all render, and the output is byte-identical tomaininevery case, so the change is behaviour-preserving on the happy path.
flake8andpython-blackpass at the versions the Zuul jobs pin.In CI, on patchset
321fdecosism/checkis green — all 10 jobs(buildset).
Three of them execute the changed code rather than just linting it, each
against live GitHub with a different pin:
MANAGER_VERSIONgenerics-tox-test-latestlatestgenerics-tox-test-stable9.1.0generics-tox-test-stable-legacy9.0.0In
generics-tox-test-latest,python3 src/render-images.pyexits 0 in 1.15sand the
cat images.ymlthat follows it is byte-identical to the 56 linesmainrenders. These three jobs were themselves exposed to this bug wheneverCI tripped the rate limit; they now exercise the fix on every change.
Limitation
No automated test accompanies this. There is no tracked test suite for these
scripts, and the failure needs an HTTP server returning controlled statuses to
exercise; the verification above was run by hand.
🤖 Generated with Claude Code