Expand description
This gateway is deliberately client-secret-free, as D3 requires.
Every read model the dashboard and the CLI display, over api.github.com:
the runner inventory, the in-progress workflow count, and the runner-package
download metadata — plus the two behaviours that make those numbers
trustworthy rather than merely present.
Everything here is built on crate::AuthenticatedClient. There is no
second authentication path in this module and none may be added; the one
credential is obtained by crate::device_flow and applied by that client.
§The three read models
| Operation | Endpoint | Type |
|---|---|---|
InventoryGateway::list_runners | /repos/{o}/{r}/actions/runners, /orgs/{org}/actions/runners | RunnerInventory |
InventoryGateway::in_progress_activity | /repos/{o}/{r}/actions/runs?status=in_progress | ActivityCount |
InventoryGateway::runner_downloads | …/actions/runners/downloads | RunnerDownloads |
The in-progress workflow count and the busy-runner count are different
numbers with different meanings, and this module keeps them in different
types on purpose. A workflow run is work GitHub has accepted; a busy runner
is a machine this product can see executing something. g2 renders them as
separate aggregates, and collapsing them here would make that impossible to
do correctly downstream.
§Pagination is mandatory
04-subsystem-contracts.md: “Pagination is mandatory; the dashboard must not
treat a first page as a complete inventory.” A target with more runners than
one page is the ordinary case for an organization, and a silently truncated
list reads as “no runners” rather than as an error — the failure is invisible
at exactly the moment it matters.
Every collection here therefore follows Link: rel="next" through
crate::ApiResponse::next_page, which is c2’s single reader of that
header rather than a second one written here. Following the same reader is
the point: it already handles a rel="next" that is not first, quoted and
unquoted parameter forms, and — the case that silently stopped pagination at
page one until a review caught it — a next-page URL that itself contains a
comma, which a runner query carries routinely as labels=self-hosted,windows.
Two facts travel with a collection so that a caller can tell a complete
answer from an incomplete one: RunnerInventory::reported_total, which is
GitHub’s own total_count, and RunnerInventory::truncated, which is set
when the crate::MAX_PAGES ceiling stopped the walk.
§Rate limiting is a policy, and it lives here
c2 deliberately implemented none of it — it stopped discarding the
evidence and handed it across the seam through GithubError::headers,
GithubError::retry_after and GithubError::rate_limit. This module is
where the evidence becomes a decision, and the decision has three parts:
retry-afteris obeyed by not sending anything. A detected limit latches a window (RestInventory::rate_limit_backoff) during which this gateway opens no socket at all and answersInventoryError::RateLimitedimmediately. Obeying a back-off by sleeping inside a request would be the same wait, spent invisibly, with the caller’s cancellation and refresh scheduling both bypassed.- It is surfaced, never hidden (
04-subsystem-contracts.md, “Rate limiting increases the refresh delay and is displayed, never hidden”).RateLimitedis a displayable state carrying what GitHub said, andRefreshState::retry_delayis the absolute floor on whene1may try again —next_attempt_at = now + retry_delay, not the ordinary interval plus that. Adding it would only wait longer than necessary: this gateway already enforces the window itself, at no request cost. - A rate limit is never confused with a permissions answer. See
RateLimited::detect: GitHub sendsx-ratelimit-*on every response, so “remaining is zero” alone would turn an ordinary404into a rate limit.
§The shared request budget (the D4 consequence)
Under scale sets, demand arrived over a long poll carried by the Actions
service, which did not touch the api.github.com budget. After D4 it does,
and that makes one number a product constraint rather than an implementation
detail: demand, runner inventory and in-progress counts all draw on one
ceiling of HOURLY_REQUEST_CEILING requests per hour.
The projection lives here because this is the layer that sees every request.
See TargetCost and BudgetProjection — and in particular
TargetCost::organization, because an organization target’s cost scales
with the number of repositories the App is installed on there. Projecting an
organization as a flat per-target constant understates its real cost by
exactly that factor, which is the one error this model exists to prevent.
Structs§
- Activity
Count - In-progress workflow runs, per repository and in total.
- Activity
Scope - Which repositories one activity count covers.
- Budget
Projection - What a host’s configured target set will cost per hour, and whether that fits.
- Cancel
Token - A latch a caller flips to stop in-flight gateway work.
- Inventory
Snapshot - One target’s read models, as of one instant.
- Rate
Limit Headroom - What GitHub last said about this credential’s hourly quota, read from a response that succeeded.
- Rate
Limited - An exhausted rate limit, as a state something can display.
- Refresh
Coalescer - Runs one refresh at a time; a refresh asked for while another is in flight joins it instead of issuing a second.
- Rest
Inventory InventoryGatewayoverapi.github.com.- Runner
- One self-hosted runner GitHub knows about, local or not.
- Runner
Download - One runner-package download GitHub publishes.
- Runner
Downloads - Every runner package GitHub publishes for a target.
- Runner
Inventory - Every runner GitHub reports for one target, across every page.
- Target
Cost - What one target costs, per refresh, in requests against the shared ceiling.
- Unavailable
Repository - A repository the aggregate could not read, and why.
Enums§
- Admission
- The answer
f2’srepo addandorg addact on. - Inventory
Error - Everything an inventory read can fail with.
- Rate
Limit Kind - Which of GitHub’s two rate limits a response was attributed to.
- Refresh
State - One refresh’s outcome, as a value that can be stored, compared and rendered.
- Runner
Status - A runner’s connection state, as GitHub reports it.
Constants§
- ACTIVITY_
REQUESTS_ PER_ REPOSITORY_ PER_ REFRESH - Requests one in-progress workflow count costs, per repository.
- BUDGET_
SHARE_ DIVISOR - The fraction of
HOURLY_REQUEST_CEILINGa host may plan to spend. - DEFAULT_
RATE_ LIMIT_ BACKOFF - How long a detected rate limit backs off for when GitHub gives no usable
retry-afterand nox-ratelimit-reset. - DEMAND_
REQUESTS_ PER_ REPOSITORY_ PER_ REFRESH - Requests one demand poll costs, per repository: the queued runs, then their jobs.
- HOURLY_
REQUEST_ CEILING - The documented hourly REST ceiling for a user-to-server token, measured
2026-08-21 (
04-subsystem-contracts.md). - MAX_
ACTIVITY_ FALLBACK_ PAGES - The most pages one repository’s in-progress count may walk when GitHub sends
no
total_count. - MAX_
RATE_ LIMIT_ BACKOFF - The longest a rate limit may silence this gateway, whatever GitHub asked for.
- PER_
PAGE - Items per page asked for on every paginated call.
- RUNNER_
INVENTORY_ REQUESTS_ PER_ REFRESH - Requests one runner-inventory refresh costs, per target.
- SECONDS_
PER_ HOUR - Seconds in the hour the ceiling is measured over.
Traits§
- Inventory
Gateway - Every read model the dashboard and the CLI display.
Functions§
- budget_
allowance - The requests per hour a host may plan to spend: half the documented ceiling.
- refreshes_
per_ hour - Refreshes one hour holds at
interval.