Skip to main content

Module rest

Module rest 

Source
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

OperationEndpointType
InventoryGateway::list_runners/repos/{o}/{r}/actions/runners, /orgs/{org}/actions/runnersRunnerInventory
InventoryGateway::in_progress_activity/repos/{o}/{r}/actions/runs?status=in_progressActivityCount
InventoryGateway::runner_downloads…/actions/runners/downloadsRunnerDownloads

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:

  1. retry-after is 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 answers InventoryError::RateLimited immediately. 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.
  2. It is surfaced, never hidden (04-subsystem-contracts.md, “Rate limiting increases the refresh delay and is displayed, never hidden”). RateLimited is a displayable state carrying what GitHub said, and RefreshState::retry_delay is the absolute floor on when e1 may 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.
  3. A rate limit is never confused with a permissions answer. See RateLimited::detect: GitHub sends x-ratelimit-* on every response, so “remaining is zero” alone would turn an ordinary 404 into 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§

ActivityCount
In-progress workflow runs, per repository and in total.
ActivityScope
Which repositories one activity count covers.
BudgetProjection
What a host’s configured target set will cost per hour, and whether that fits.
CancelToken
A latch a caller flips to stop in-flight gateway work.
InventorySnapshot
One target’s read models, as of one instant.
RateLimitHeadroom
What GitHub last said about this credential’s hourly quota, read from a response that succeeded.
RateLimited
An exhausted rate limit, as a state something can display.
RefreshCoalescer
Runs one refresh at a time; a refresh asked for while another is in flight joins it instead of issuing a second.
RestInventory
InventoryGateway over api.github.com.
Runner
One self-hosted runner GitHub knows about, local or not.
RunnerDownload
One runner-package download GitHub publishes.
RunnerDownloads
Every runner package GitHub publishes for a target.
RunnerInventory
Every runner GitHub reports for one target, across every page.
TargetCost
What one target costs, per refresh, in requests against the shared ceiling.
UnavailableRepository
A repository the aggregate could not read, and why.

Enums§

Admission
The answer f2’s repo add and org add act on.
InventoryError
Everything an inventory read can fail with.
RateLimitKind
Which of GitHub’s two rate limits a response was attributed to.
RefreshState
One refresh’s outcome, as a value that can be stored, compared and rendered.
RunnerStatus
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_CEILING a host may plan to spend.
DEFAULT_RATE_LIMIT_BACKOFF
How long a detected rate limit backs off for when GitHub gives no usable retry-after and no x-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§

InventoryGateway
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.