Skip to content

Add drift-detection tooling (check-drift) to the release repo#2561

Merged
berendt merged 12 commits into
mainfrom
drift-detection
Jul 7, 2026
Merged

Add drift-detection tooling (check-drift) to the release repo#2561
berendt merged 12 commits into
mainfrom
drift-detection

Conversation

@ideaship

@ideaship ideaship commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What

Adds check-drift.py, a plugin-based drift detector for OSISM's image and
version wiring, and wires it into CI. It catches divergence — when it is
introduced
— before it breaks a deploy or a gate, across two check groups
(--group image | kolla | all).

Image version pins — follows an image's version down the chain
release base.yml → rendered manager images.yml → role defaults:

  • release_vs_manager — the rendered manager images.yml disagrees with the release pin
  • role_shadows — a role-default tag shadows the release pin (LIVE = the role default actually deploys; DORMANT = the release pin wins)
  • role_unpinned — a role-default tag with no release pin
  • image_orphan — a manager image alias is emitted but consumed by no role or playbook

Kolla container-image toolchain — divergence between the OSISM consumer repos
(defaults, release, generics, the container-image repos) and upstream
openstack/kolla / openstack/kolla-ansible:

  • kolla_enablement_orphan — OSISM enables a service upstream no longer defines
  • kolla_groupvars_missing — an upstream group_vars/all var OSISM's defaults never mirrored, so it is undefined at deploy/upgrade (the add-direction mirror of kolla_enablement_orphan; the class behind the 2025.2 keystone_listen_port break)
  • kolla_orphan_config — dead companion vars / image blocks for an orphaned service
  • kolla_secrets_orphan — an OSISM kolla secret with no upstream counterpart
  • kolla_enablement_build — an enabled, upstream-buildable service missing from the build set
  • kolla_version_chain_upstream — an upstream docker/ service with no version-pin line
  • kolla_version_chain_inner — a referenced version pin that resolves to nothing (inert pin)
  • kolla_inventory — an upstream inventory group missing from the OSISM 50/51-kolla files

The framework lands first (empty registry, flags nothing on its own), then the
plugins in lifecycle order, each reviewable in isolation. Sources resolve
remotely (GitHub) by default, so the detector runs anywhere including CI;
--base-dir reads local checkouts. An allowlist marks intentional exceptions
(each requiring a reason); entries that match nothing are reported stale, so
suppressions can't silently rot. Exit codes: 0 clean / 1 drift or stale / 2 input
error. Full reference: docs/check-drift-image.md, docs/check-drift-kolla.md
(incl. the osism/defaults release/bake model and why the range-aware checks
compare against the union of supported releases).

Commits (each independently green — unit tests pass at every commit)

  • drift: add plugin framework and driver
  • drift: add image-drift plugins
  • drift: add kolla-drift plugins
  • drift: add drift-detection docs
  • drift: wire tox and Zuul CI
  • kolla: read enable flags from all defaults files — make the enable-flag readers layout-agnostic (read enable_* from the union of all/*.yml, not just 099-kolla.yml), so a defaults reorganization can't silently drop a flag
  • docs: explain defaults bake model and unions
  • kolla: add kolla_groupvars_missing drift check — the add-direction group_vars check (both OSISM delivery paths counted: osism/defaults + the rendered container-image-kolla-ansible versions.yml)
  • kolla: allowlist eutron_external_interface typo

CI

  • release-tox-drift-test — the unit suite (tox -e drift-test, 248 tests).
    Runs in check (gates this PR) and periodic-daily.
  • release-tox-drift — the detector (tox -e drift, check-drift.py --group all). Runs periodic-daily only: it reads live upstream state, so
    it's unfit as a PR gate. It exits non-zero when drift exists, so the daily job
    goes red with the report in its log.

What this finds in practice

Run against the live OSISM repos today, the detector reports 73 actionable
items, 42 allowlisted, 0 stale
(115 total):

  • image pins — 1 rendered-vs-release disagreement (ara_server), 3 LIVE
    role-default shadows that actually deploy (dnsmasq, scaphandre, stepca),
    9 DORMANT lags, 4 unpinned tags (httpd, httpd_data, kepler,
    thanos_sidecar), and 3 orphaned manager aliases (nginx, osism_netbox,
    registry).
  • kolla toolchain — now surfaces real drift (it was reported clean before
    this branch). The layout-agnostic enable-flag reader scans enable_* across
    all osism/defaults all/*.yml files (not just 099-kolla.yml), surfacing
    9 enablement orphans (auditd, chrony, openstack_exporter, karbor,
    qinling, searchlight, three horizon_*) and their 9 dead companion vars
    — some are removed-upstream projects to drop, some are OSISM inventions to
    allowlist. The new add-direction kolla_groupvars_missing surfaces 35
    upstream group_vars/all keys OSISM never mirrored
    — the osism/defaults
    re-sync backlog behind the 2025.2 keystone_listen_port upgrade break, incl.
    enable_ironic_pxe_filter and the nova_database_*/nova_cell0_database_*
    batch. eutron_external_interface is allowlisted (upstream typo for
    neutron_external_interface, awaiting
    https://review.opendev.org/c/openstack/kolla-ansible/+/985031).

Earlier, the removal-direction kolla checks already drove a wave of cleanups
across the consumer repos (kolla actionable-drift 81 → 0 at the time, all merged);
this branch's layout-agnostic reader and new add-direction check now surface a
further backlog — chiefly the osism/defaults re-sync the keystone_listen_port
break called for:

It self-audits its allowlist too: an entry that no longer suppresses real drift is
a hard error (exit 1), so suppressions can't silently rot.

Because the detector is daily-only, this output does not appear in this PR's
checks. Full release-tox-drift report against current upstream
(check-drift.py --group all):

Checks follow an image's version pin: release base.yml → rendered manager images.yml → role defaults.

release_vs_manager — 1 image tags in the rendered manager images.yml
disagree with the release base.yml pins:

    ara_server

  Fix: re-render environments/manager/images.yml from the current release,
       or allowlist the entry if the divergence is intentional (e.g. a
       rolling-release image pinned to 'latest').
  Refs: release/latest/base.yml
        testbed/environments/manager/images.yml

role_shadows — 9 DORMANT — overridden by the rendered images.yml; the
release pin wins at deploy:

    adminer_tag (4.7 → 5.4.2)   roles/adminer/defaults/main.yml
    ara_server_mariadb_tag (11.8.3 → 11.8.4)   roles/manager/defaults/main.yml
    manager_redis_tag (7.4.6-alpine → 7.4.7-alpine)   roles/manager/defaults/main.yml
    vault_tag (1.21.0 → 1.21.4)   roles/manager/defaults/main.yml
    netbox_redis_tag (7.4.6-alpine → 7.4.7-alpine)   roles/netbox/defaults/main.yml
    postgres_tag (16.10-alpine → 16.11-alpine)   roles/netbox/defaults/main.yml
    nexus_tag (3.82.0 → 3.93.1)   roles/nexus/defaults/main.yml
    phpmyadmin_tag (5.2 → 5.2.3)   roles/phpmyadmin/defaults/main.yml
    traefik_tag (v3.4.4 → v3.5.0)   roles/traefik/defaults/main.yml

  Fix: lower priority; sync when convenient.
  Refs: release/latest/base.yml

role_shadows — 3 LIVE — no images.yml override; the role default is what
actually deploys:

    dnsmasq_tag (2.90 → 2.91)   roles/dnsmasq/defaults/main.yml
    scaphandre_tag (1.0.0 → 1.0.2)   roles/scaphandre/defaults/main.yml
    stepca_tag (0.28.4 → 0.30.2)   roles/stepca/defaults/main.yml

  Fix: add `<alias>_tag`/`<alias>_image` to the manager render template
       (images.yml.j2) so the latest/base.yml pin governs the deployed
       version.
  Refs: release/latest/base.yml

role_unpinned — 4 <alias>_tag pins in role defaults with no release base.yml
pin:

    httpd_tag (alpine, no release pin)   roles/httpd/defaults/main.yml
    httpd_data_tag (latest, no release pin)   roles/httpd/defaults/main.yml
    kepler_tag (v0.11.2, no release pin)   roles/kepler/defaults/main.yml
    thanos_sidecar_tag (v0.32.5, no release pin)   roles/thanos_sidecar/defaults/main.yml

  Fix: add a pin to release base.yml (and wire <alias>_tag into the manager
       render template) to make it release-managed, or allowlist it if the
       image is intentionally role-managed.
  Refs: release/latest/base.yml

image_orphan — 3 image vars emitted by the manager images.yml that no role
or playbook consumes:

    nginx, osism_netbox, registry

  Fix: remove the orphaned <alias>_tag/<alias>_image (and its etc/images.yml
       + role-default remnants), or allowlist it if the image is genuinely
       consumed in a form this scan misses — it flags an alias only when no
       literal {{ <alias>_image }} reference is found in the scanned
       roles/playbooks.
  Refs: ansible-collection-services/roles/, ansible-playbooks-manager/playbooks/
        generics/environments/manager/images.yml

Checks follow a service's path: enabled → built → version-pinned → deployed.

kolla_enablement_orphan — 9 OSISM enable flags whose service upstream
kolla-ansible no longer defines at any supported release; the service was
removed or renamed upstream, leaving the flag orphaned:

    auditd, chrony, horizon_karbor, horizon_qinling, horizon_searchlight,
    karbor, openstack_exporter, qinling, searchlight

  Fix: remove the stale enable_<name> from osism/defaults, or migrate it to
       the upstream replacement. Some flags (e.g. common, kolla_operations)
       are OSISM inventions with no upstream counterpart — keep those
       allowlisted rather than removed.
  Refs: openstack/kolla-ansible group_vars enable-defaults @ supported refs
        osism/defaults all/*.yml

kolla_groupvars_missing — 35 upstream kolla-ansible group_vars OSISM
defaults never mirrored; each is undefined in the deploy var context and
aborts any role task that uses it:

    ceph_cinder_backup_keyring, ceph_cinder_keyring, ceph_glance_keyring,
    ceph_gnocchi_keyring, ceph_manila_keyring, ceph_nova_keyring,
    distro_python_version_map, enable_cinder_backend_hnas_nfs,
    enable_ironic_pxe_filter, enable_swift_recon, enable_swift_s3api,
    glance_backend_swift, keystone_listen_port, nova_cell0_database_address,
    nova_cell0_database_name, nova_cell0_database_password,
    nova_cell0_database_shard_id, nova_cell0_database_user,
    nova_database_address, nova_database_name, nova_database_user,
    prometheus_msteams_port, prometheus_msteams_webhook_url,
    swift_account_server_port, swift_container_server_port,
    swift_external_fqdn, swift_internal_base_endpoint,
    swift_internal_endpoint, swift_internal_fqdn, swift_object_server_port,
    swift_proxy_server_listen_port, swift_proxy_server_port,
    swift_public_base_endpoint, swift_public_endpoint, swift_rsync_port

  Fix: mirror the missing var into osism/defaults all/*.yml (copying
       upstream's definition — harmless when the service is off, needed when
       an environment enables it), or allowlist it with a reason if OSISM
       deliberately omits it: the related service is not shipped/supported
       by OSISM at all (so the var is never evaluated anywhere), a var OSISM
       supplies another way, or an upstream typo.
  Refs: openstack/kolla-ansible group_vars/all @ supported refs
        osism/defaults all/*.yml

kolla_orphan_config — 2 dead companion/image config vars for services
upstream removed (their enable_<name> flag is reported separately by
kolla_enablement_orphan); these must be removed too:

    chrony_image, chrony_tag

  Fix: remove these vars from the listed osism/defaults file, or allowlist
       any that are intentionally kept (an OSISM invention with no upstream
       service).
  Refs: openstack/kolla-ansible (service removed upstream)
        osism/defaults all/002-images-kolla.yml

kolla_orphan_config — 2 dead companion/image config vars for services
upstream removed (their enable_<name> flag is reported separately by
kolla_enablement_orphan); these must be removed too:

    chrony_allowed_subnets, chrony_servers

  Fix: remove these vars from the listed osism/defaults file, or allowlist
       any that are intentionally kept (an OSISM invention with no upstream
       service).
  Refs: openstack/kolla-ansible (service removed upstream)
        osism/defaults all/099-generic.yml

kolla_orphan_config — 5 dead companion/image config vars for services
upstream removed (their enable_<name> flag is reported separately by
kolla_enablement_orphan); these must be removed too:

    openstack_exporter_clouds_yml_cloud, openstack_exporter_clouds_yml_path,
    openstack_exporter_secure_yml_path,
    openstack_exporter_service_monitor_scrape_interval,
    openstack_exporter_service_monitor_scrape_timeout

  Fix: remove these vars from the listed osism/defaults file, or allowlist
       any that are intentionally kept (an OSISM invention with no upstream
       service).
  Refs: openstack/kolla-ansible (service removed upstream)
        osism/defaults all/099-infrastructure.yml

Summary: 73 to act on, 42 allowlisted, 0 stale allowlist entries (115 total)

How to read it

  • image pins — DORMANT role_shadows lag but the release pin governs at
    deploy (low priority); LIVE role_shadows (dnsmasq, scaphandre, stepca)
    actually deploy the role default (wire into images.yml.j2); role_unpinned
    (httpd*, kepler, thanos_sidecar) have no release pin; image_orphan
    (nginx, osism_netbox, registry) are emitted but consumed nowhere.
  • kolla removal-directionkolla_enablement_orphan + kolla_orphan_config:
    stale enable_* flags and their companion vars for services upstream dropped;
    drop the removed projects, allowlist the OSISM inventions.
  • kolla add-directionkolla_groupvars_missing: the osism/defaults
    re-sync backlog. The right fix for most is to mirror the upstream var into
    osism/defaults (harmless when the service is off, correct when an environment
    enables it — e.g. metalbox turns on ironic, so enable_ironic_pxe_filter bites
    there); allowlist only genuine divergences (the eutron typo).

The detector runs unauthenticated against GitHub (no token support in
source.py), same as generics' existing daily job. A heavy run can occasionally
hit API rate limits and fail without a report; adding GITHUB_TOKEN support is a
sensible follow-up.

@ideaship ideaship force-pushed the drift-detection branch 2 times, most recently from 8154aeb to 12c2956 Compare July 2, 2026 07:02
@berendt berendt moved this from New to In progress in Human Board Jul 2, 2026
@ideaship ideaship marked this pull request as ready for review July 2, 2026 07:39
@ideaship ideaship marked this pull request as draft July 2, 2026 08:22
@ideaship ideaship marked this pull request as ready for review July 2, 2026 10:40
@ideaship ideaship requested a review from berendt July 2, 2026 10:40
@berendt berendt moved this from In progress to Ready for review in Human Board Jul 2, 2026
@ideaship ideaship moved this from Ready for review to In review in Human Board Jul 4, 2026
ideaship and others added 9 commits July 7, 2026 16:14
Introduce the osism_drift framework that later plugins build on. It
exists to catch OSISM version-chain drift automatically instead of at
deploy/run time.

- source: read a file or list a directory from any OSISM repo,
  transparently from a local checkout (--base-dir) or, with none, from
  GitHub raw + API. Pinned upstream repos are read at explicit historical
  refs; consumer repos are read from the working tree / remote head.
- model.DriftEntry: one disagreement between an authoritative source and
  a consumer, with optional per-entry summary/remediation overrides.
- driver: selects enabled plugins (or --plugin), applies the allowlist,
  renders a grouped lifecycle-ordered report (text or JSON), flags stale
  allowlist entries, and exits 0 clean / 1 drift-or-stale / 2 input error
  so it maps onto a CI gate. It is plugin-agnostic: plugin groups and
  report headers are passed in.
- config + allowlist loaders: per-plugin enable flags; every allowlist
  entry needs a reason; unmatched entries are stale.
- demo: illustrative report built from each plugin's live metadata,
  short-circuiting before any config/source/allowlist load.

Plugins, their config entries and allowlist entries follow in later
commits.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Roger Luethi <luethi@osism.tech>
Image-version-pin checks. An image's version flows release base.yml →
rendered manager images.yml → role defaults; these plugins find where
the chain disagrees.

- release_vs_manager: release pin vs the rendered manager images.yml.
- role_shadows: role-default <alias>_tag that disagrees with the release
  base.yml pin. A LIVE finding (alias not overridden by images.yml.j2)
  is fixed by wiring the alias into the manager render template
  images.yml.j2 so the release pin governs -- NOT by editing the role
  default, which must never govern the deployed version.
- role_unpinned: role-default tag with no canonical release pin.
- image_orphan: a manager-template image alias no role or playbook
  consumes. Scans ansible-collection-services/roles and
  ansible-playbooks-manager/playbooks; only .yml/.yaml/.j2 files.

role_shadows and role_unpinned skip stream-resolved aliases
(ceph_ansible, kolla_ansible, the osism family) that resolve via
<name>_version at deploy time.

Allowlist: rolling-release osism images (release_vs_manager) and the
OSISM CI/zuul stack whose versions live in the zuul role, not the
release (osism/issues#1397).

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Roger Luethi <luethi@osism.tech>
Checks for the kolla container-image version chain: openstack/kolla
defines images (docker/ dirs) -> container-images-kolla pins concrete
versions (SBOM map) -> container-image-kolla-ansible consumes them via
versions.yml.j2. Plugins are registered in lifecycle order (enabled ->
built -> version-pinned -> deployed):

- kolla_enablement_orphan: OSISM enable_* flags whose service is gone
  from upstream across the supported release range.
- kolla_orphan_config: leftover config vars of those dead services.
- kolla_secrets_orphan: OSISM kolla secrets with no upstream counterpart.
- kolla_enablement_build: OSISM-enabled, upstream-buildable services
  missing from the release build set.
- kolla_version_chain_upstream: kolla docker/ services with no
  versions['K'] line in the kolla-ansible template.
- kolla_version_chain_inner: versions['K'] keys the SBOM map never emits
  (inert pins); classified add-vs-remove by whether OSISM builds the image.
- kolla_inventory: upstream multinode groups absent from the OSISM
  50/51-kolla inventory (configparser, strict=False for dup hosts).

Enable-defaults layout changes at 2025.2 (monolithic group_vars/all.yml
-> split group_vars/all/*.yml). zun ships a kolla image but OSISM does
not build it; valkey is intentionally left to flag ahead of enablement.
Allowlists use exact matches for the underscore-namespaced plugins and
prefix (- / :) only for inventory group names.

This commit also lands the shared driver-group/demo test and the report
test that renders a kolla plugin, so the full suite is green here.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Roger Luethi <luethi@osism.tech>
Reference docs for the two check groups, presenting each plugin in
lifecycle order under a uniform template, plus an "adding a plugin"
guide (allowlist.apply idiom) and the exit-code contract.

- docs/check-drift-image.md
- docs/check-drift-kolla.md

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Roger Luethi <luethi@osism.tech>
Add the tox envs and Zuul jobs to run the tooling.

- tox -e drift-test: the unit suite (pytest tests/).
- tox -e drift: the detector, defaulting to --group all so the periodic
  job needs no posargs; --group stays required on the direct CLI.
- .zuul.yaml: release-tox-drift-test runs in check and periodic-daily;
  release-tox-drift runs in periodic-daily only, because it inspects live
  upstream state and is unfit as a PR gate.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Roger Luethi <luethi@osism.tech>
The three kolla plugins that inspect enable_* flags
(kolla_enablement_orphan, kolla_enablement_build, and
kolla_version_chain_inner) each read them from a single hardcoded
file, osism/defaults all/099-kolla.yml. That couples the detector to
one file layout: if a future osism/defaults reorganization moves an
enable_<id> into another file (for example a per-service split
mirroring upstream kolla-ansible's 2025.2 group_vars/all/*.yml
restructuring), the flag would silently drop out of the comparison and
produce false negatives or false positives.

Add a shared helper, enablement.osism_enable_flags(config), that reads
the union of enable_* flags across every osism/defaults all/*.yml file
(skipping non-.yml entries), mirroring how kolla_orphan_config already
globs the directory. Switch all three plugins to it and drop their
per-module _ENABLE constants. The comparison now depends on variable
names, not on which file a flag lives in, so the kolla detector is
immune to any defaults file reorganization.

Behaviour is unchanged for the current layout, since 099-kolla.yml is
today the only defaults file carrying enable_* flags. Update INPUT_FILES
and docs/check-drift-kolla.md to reflect all/*.yml.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Roger Luethi <luethi@osism.tech>
The range-aware kolla plugins read osism/defaults at its single tip and
compare against the union of upstream refs across the supported release
range, but the docs never said why. Without that rationale a plugin
author cannot tell whether union, intersection, or newest-release is
correct for a new check.

Add a "Release model and why the range-aware checks use unions"
subsection documenting the model: osism/defaults is a single-tip repo
(dated tags, no per-release branches); a defaults tag is baked into each
osism/release manifest at release-cut time; and one defaults_version is
shared across every supported OpenStack release in a manager version, so
that one snapshot must be correct for all of them at once. Hence both
directions compare against the union — removal-direction to avoid false
orphans, add-direction because the tip must satisfy every release. Also
cross-reference it from the range-aware note in kolla_enablement_build.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Roger Luethi <luethi@osism.tech>
The three defaults<->upstream kolla plugins all run the removal
direction: they flag things OSISM ships that upstream no longer defines.
Nothing checked the opposite (add) direction, so an upstream global var
that osism/defaults never mirrored went unnoticed until it broke a
deploy. This is the class behind the 2025.2 keystone_listen_port
failure: added upstream in the uWSGI migration, never picked up by
osism/defaults, so the keystone upgrade aborted on an undefined variable.

Add kolla_groupvars_missing, the add-direction mirror of
kolla_enablement_orphan. It diffs key spaces: the union of upstream
kolla-ansible group_vars/all top-level keys across the supported release
range MINUS everything OSISM supplies to the container's group_vars/all.
OSISM supplies from two paths, both counted: osism/defaults all/*.yml
(via the generics gilt overlay) and the rendered
container-image-kolla-ansible versions.yml (openstack_release, the
kolla_*_version pins, ...) -- a var supplied only by the latter must not
false-positive as missing. Union across releases because one
osism/defaults snapshot is baked per manager version and must satisfy
every supported OpenStack release at once (see the release-model note in
docs/check-drift-kolla.md); top-level group_vars only, keys compared
verbatim (an Ansible var name is an exact identifier). Deliberate
divergences (an upstream typo, a var OSISM supplies another way) are
carried in the allowlist with a reason.

Extract the shared readers behind the enablement helpers -
top_level_keys, plus _osism_defaults_bodies and _upstream_groupvars_bodies
feeding both the enable-flag and the group_vars key sets - so the new
check reuses the monolithic/split group_vars probe and the all/*.yml
union. Register the plugin in the config stage of the kolla lifecycle,
right after kolla_enablement_orphan, enable it in the default config, and
document it in docs/check-drift-kolla.md.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Roger Luethi <luethi@osism.tech>
kolla_groupvars_missing flags eutron_external_interface, an upstream
kolla-ansible group_var that OSISM does not mirror. It is an upstream
typo for neutron_external_interface (which OSISM defines correctly), so
mirroring it would be wrong. Allowlist it with a reason pointing at the
upstream fix under review; once that merges the key disappears upstream
and the check reports this entry as stale, prompting its removal.

Waiting for: https://review.opendev.org/c/openstack/kolla-ansible/+/985031

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Roger Luethi <luethi@osism.tech>
@berendt berendt force-pushed the drift-detection branch from 01ce5ea to fc13e29 Compare July 7, 2026 14:45
ideaship added 3 commits July 7, 2026 17:23
release_to_ref re-probed upstream refs on every call, so each kolla
plugin that resolves a release independently repeated the same
ref_exists commits-API requests -- the docstring's "resolved once per
run" was aspirational. Cache resolved refs on a new Config.ref_cache
dict keyed by (repo, release). Config is frozen, but mutating the dict
it holds is allowed; a fresh Config per run (and per test) isolates the
cache. The release_refs override path is unchanged.

Measured effect on an unauthenticated remote --group kolla run: 42 ->
32 api.github.com requests (10 redundant ref_exists probes removed),
which matters against GitHub's 60/hr per-IP unauthenticated ceiling.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Roger Luethi <luethi@osism.tech>
Remote reads were always unauthenticated, capping the drift detector at
GitHub's 60/hr per-IP limit. When GITHUB_TOKEN (or GH_TOKEN) is present
in the environment, send it as a bearer token on every raw and API
request, lifting the ceiling to 5000/hr; when absent, behaviour is
unchanged. The Zuul periodic-daily drift job carries no token today, so
this is a no-op there; it helps local/developer runs and any future
authenticated CI.

remote.github_raw / remote.github_api are configurable (a mirror, proxy,
or test server), so the token is attached only when the request host is
github.com / api.github.com / raw.githubusercontent.com. This keeps an
ambient token from leaking to a non-GitHub endpoint someone pointed the
base URLs at; the default config targets public GitHub, so authenticated
runs there are unchanged. _auth_headers takes the request url; all seven
call sites pass it.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Roger Luethi <luethi@osism.tech>
A remote drift run makes many GitHub requests, and an
unauthenticated run shares a low per-IP hourly budget it can
exhaust. When that happened the source layer raised a bare
"HTTP 403 fetching <url>", giving the user no clue it was
throttling rather than a real failure, nor that setting a token
would help.

Add _rate_limit_hint(), which recognizes GitHub's throttling
signals -- 403/429 with X-RateLimit-Remaining: 0 (primary limit)
or a Retry-After header (secondary limit) -- and builds an
actionable hint: when to retry (from X-RateLimit-Reset or
Retry-After) and, for unauthenticated runs, advice to set
GITHUB_TOKEN/GH_TOKEN. A plain 403 with no such marker (e.g. a
permission refusal) is left as an unadorned HTTP error so the
hint never misleads.

Route all remote request sites through a shared _http_error()
helper so every one of them gains the hint instead of only one.

The quota figures are not hardcoded: the message echoes the
X-RateLimit-Limit value the server reported for that request, so
it stays accurate whenever the script runs even if GitHub changes
its limits.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Roger Luethi <luethi@osism.tech>
@berendt berendt merged commit d3bac8c into main Jul 7, 2026
2 checks passed
@berendt berendt deleted the drift-detection branch July 7, 2026 16:29
@github-project-automation github-project-automation Bot moved this from In review to Done in Human Board Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants