GitHub
10/23/2026, 8:41 PMrollout object with state (running or paused), stage (1-based stage the host joins), stages (percent per stage) and eligible_at (when that stage opens, null when paused or already open).
• It is present only when all of these hold:
• the effective target comes from the environment, not a pin
• the latest rollout row belongs to the current target
• the rollout is running or paused
• the host is not Windows
• The stage uses the same bucket as Resolve (environment id, target version, stored upper-case uuid), so the node page and the route cannot disagree. A test ties the two together.
• eligible_at is the stored stage's open time plus the number of stage-waits ahead of it. Using the effective stage here would count elapsed stages twice.
• New Rollout.NodeStage(bucket) returns the first stage that contains a bucket.
• Wording:
• gated: "Not in the rollout yet: this host is in the 50% stage, which opens in about 1 h 20 min."
• past or under a minute: "opens shortly"
• the stage has already opened: "The rollout has reached this host's stage (50%); it will upgrade at its next sync."
• paused: the existing sentence plus the host's stage
• no rollout information: the previous generic sentences
• The relative-time helper moved into osqueryReasons.ts and the rollout panel reuses it.
Cached rollout progress
• The environment rollout view caches its progress result per environment in the API process for 5 seconds. The cache is bounded to 256 environments, safe for concurrent use, and has an injectable clock.
• The key is rollout row id, effective state and effective stage, so a stage opening or a state change misses automatically.
• Errors are never cached, and aborted rollouts (which skip the scan) are not cached. The cache sits below the permission checks, and the cached host list is copied on the way in and out.
• Set target, Clear target, Resume and Abort invalidate the environment's entry, so their responses are fresh.
• With several API replicas, another replica can show counts up to 5 seconds old after a change.
Security and operational impact
• No new endpoints, permissions or schema. The node rollout field is derived server-side. The UI interpolates only a numeric stage and a parsed date.
• Fewer database scans on the admin page. A cache hit costs one lookup.
• The OpenAPI file is regenerated for the new field.
Validation
• go test ./cmd/api/... ./pkg/... -count=1, go vet ./..., gofmt -l cmd pkg
• `golangci-lint run ./cmd/api/... ./pkg/...`: 0 issues
• `make openapi-check`: clean
• Frontend: npm test (644 tests) and npm run check
• New tests cover:
• NodeStage, including invalid thresholds
• eligible_at before and after earlier stages open
• pin, no rollout, paused, completed, Windows and mismatched request id
• the parity test with Resolve
• cache hit, miss after the TTL, invalidation by Set, Clear and Abort, key changes, the bound, and the copied host list
• each wording branch
• Not done: a manual check against a live rollout. Please look at the node page of a gated host on a staging rollout.
Notes
• The node response shows rollout for any host in a running or paused rollout, including hosts already at the target. The UI only shows the text when the host's last report is rollout_pending or rollout_paused.
• A node osquery request now fails with 500 if the rollout lookup fails, although the block is optional. It could be changed to log and omit the block.
jmpsec/osctrlGitHub
10/23/2026, 8:46 PM