Skip to content

Guard and retry the release repo fetches, drop a dead script - #618

Merged
berendt merged 2 commits into
mainfrom
guard-http-status-in-render-scripts
Sep 9, 2026
Merged

berendt merged 2 commits into
mainfrom
guard-http-status-in-render-scripts

Conversation

@ideaship

@ideaship ideaship commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Two scripts are fixed and one is deleted. render-images.py and
set-versions.py are made to check the HTTP status of the files they fetch
from the release repository; render-ansible-requirements.py held a third copy
of the same bug but has been dead since 2023, so it is removed rather than
fixed.

A 429 from raw.githubusercontent.com was parsed as if it were the versions
file. Every config build fetches base.yml and images.yml unauthenticated,
raw.githubusercontent.com rate-limits per IP, and neither render-images.py
nor set-versions.py looked at the status code before handing r.text to
yaml.full_load — so a rate limit that clears in a minute failed the build
with 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 yaml composer error about
the response body; an HTML error page parses as a bare string and fails further
down with a TypeError once it is indexed like a dict. grep -rn raise_for_status src/ returned nothing — no call site in this repo checked a
status.

What is here

Two commits, reviewable independently:

  1. Guard and retry the release repo fetches. Both scripts fetch through a
    fetch_yaml() helper — requests.Session whose adapter carries urllib3
    Retry (total=4, backoff_factor=1.5, status_forcelist 429/500/502/
    503/504) followed by raise_for_status(). A self-clearing rate limit is now
    waited out instead of failing the build; a persistent one reports its status
    and full URL.
  2. Remove the unused render-ansible-requirements.py. Dead since 2023 and
    unrelated 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.yml copies each script alone
into the operator's configuration repo, runs it there, and rms it —
render-images.py into environments/manager/, set-versions.py into
environments/. There is no importable sibling module at the point of use, so
a single shared helper is not reachable. ~20 lines are duplicated deliberately.

raise_on_status=False is deliberate. With urllib3's default, an exhausted
status_forcelist retry raises RetryError — "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 a
never-retried status produces. One error shape for every failure, and it always
names the URL.

The adapter is also mounted for http://, because VERSIONS_URL and
IMAGES_URL are environment-overridable and an http:// override would
otherwise silently skip the retries.

Why the second commit deletes a script rather than guarding it

render-ansible-requirements.py rendered
environments/manager/requirements.yml. That output file no longer exists:

Nothing has invoked it since — not gilt.yml, Makefile, tox.ini,
.zuul.yaml, environments/ or inventory/, and no other OSISM repository
refers 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.get in the
first place.

The functionality survives, reimplemented: the osism-ansible container image
carries its own render-ansible-requirements.py under files/src, which reads
base.yml from a git checkout of osism/release made during the image build and
imports no HTTP client at all. It never had this failure mode.

Verification

Against a local server reproducing the 429 pattern:

scenario result
429, 429, 200 recovers in 3.0s, returns parsed YAML
persistent 429 429 Client Error: Too Many Requests for url: ... after 21s of backoff
404 fails immediately, no pointless retries

Worst case adds ~21s per URL, and render-images.py fetches 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 to main in
every case, so the change is behaviour-preserving on the happy path.

flake8 and python-black pass at the versions the Zuul jobs pin.

In CI, on patchset 321fdec

osism/check is 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:

job MANAGER_VERSION build
generics-tox-test-latest latest 3bdbc2b7
generics-tox-test-stable 9.1.0 cf83435e
generics-tox-test-stable-legacy 9.0.0 ca7eec1f

In generics-tox-test-latest, python3 src/render-images.py exits 0 in 1.15s
and the cat images.yml that follows it is byte-identical to the 56 lines
main renders. These three jobs were themselves exposed to this bug whenever
CI 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

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 ideaship changed the title Guard and retry the release repo fetches Guard and retry the release repo fetches, drop a dead script Sep 9, 2026
@ideaship
ideaship marked this pull request as ready for review September 9, 2026 13:33
@ideaship ideaship self-assigned this Sep 9, 2026
@berendt
berendt merged commit e37fb82 into main Sep 9, 2026
3 checks passed
@berendt
berendt deleted the guard-http-status-in-render-scripts branch September 9, 2026 16:29
@github-project-automation github-project-automation Bot moved this from New to Done in Human Board Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

3 participants