runner-manager-github 0.4.6

Device flow and typed GitHub REST adapters (inventory, demand, JIT) for runner-manager.
Documentation

The GitHub gateway.

This crate holds every line of code in the product that talks to GitHub, and the crate root holds the one client all of it goes through.

  • [device_flow] — the OAuth 2.0 Device Authorization Grant, which is the only way this product ever obtains a credential (D3, D16).
  • [AuthenticatedClient] — the shared api.github.com client. Every request in this crate is built by it, which is what makes "sets X-GitHub-Api-Version and an explicit Accept" a property of the design rather than of each call site, and what lets the authentication-failure taxonomy be implemented exactly once.
  • [rest], [demand], [jit] — typed adapters owned by c3 and c4, built on [AuthenticatedClient].

Three properties this crate is required to keep

It holds no client secret, and it renews without one. This paragraph used to say the opposite — that the App opts out of user-token expiration, that no renewal token is ever issued, and that renewing would require a client secret a public client cannot hold. All three were wrong, and a test in this crate enforced the error by forbidding the word "refresh token".

The published App has user-token expiration on: a device-flow exchange returns expires_in: 28800 and a refresh_token. GitHub requires the client secret to refresh "unless the user access token was generated using the device flow", and every one of this product's is. So the credential renews itself, no server appears in the design, and the eight-hour life of an access token is invisible to a daemon that runs for months. See [AuthenticatedClient::renew_once], and docs/spikes/token-expiry-and-renewal.md for the two renewals that confirmed it on real hosts.

[AuthenticatedClient::revalidate] is what still happens on a 401 for a credential with no refresh half — every one issued before 0.1.11.

It persists nothing. [device_flow::DeviceFlow::complete] returns the token; it never writes it anywhere. The machine-scoped secret store is d2 and the wiring is f1. That boundary is why this crate has no dependency on runner-manager-platform and performs no filesystem write outside its own tests — and it is what lets the whole gateway be tested with no platform dependency at all.

It never renders a secret. The device code, the user access token, and every header carrying either are absent from Debug, from Display, from errors, and from tracing output. Every type here that holds one wraps it in [secrecy::SecretString] and implements [fmt::Debug] by hand, because a #[derive(Debug)] added later to a struct with a plain String field is precisely how this control is lost. tests/no_secret_reaches_the_logs.rs drives a whole login and an authenticated round trip through a capturing tracing subscriber and fails if any of the three appears.

That scan is a separate test binary, and deliberately so. As a unit test it silently stopped working: tracing caches each callsite's Interest process-wide while with_default installs a subscriber on one thread, and run concurrently with the crate's other unit tests the scan captured only its own handful of events — passing with a real device-code leak on the live path. A binary holding one test has no concurrency to be poisoned by. The word "concurrently" is load-bearing and was measured; tests/no_secret_reaches_the_logs.rs records the numbers and what they rule out.