feat(ci): runner cockpit — waiting queue, fleet rows, admin restart #556

Merged
gmackie merged 1 commit from feat/ci-runner-cockpit into main 2026-09-07 19:24:16 +00:00
Owner

Summary

Forgejo jobs waiting for a runner are now a stored snapshot (ci_queue_jobs), not a builds count. Web /ci/runners and the mobile CI tab show waiters grouped by label next to the runners that can claim them. Admins can restart a mapped forgejo-runner.

Why

The builds table under-reports waiters (changeset_id required, live stream capped at 20). That is how a fleet can look healthy while dozens of jobs sit waiting.

Schema

Additive only: 0101_ci_queue_jobs.sql (new table + capacity / has_memory_cap on ci_runners).

After merge

  1. Apply the SQL if not already applied.
  2. Deploy web.
  3. Roll the agent so the 60s reporter sends jobs.
## Summary Forgejo jobs waiting for a runner are now a stored snapshot (`ci_queue_jobs`), not a `builds` count. Web `/ci/runners` and the mobile CI tab show waiters grouped by label next to the runners that can claim them. Admins can restart a mapped `forgejo-runner`. ## Why The builds table under-reports waiters (`changeset_id` required, live stream capped at 20). That is how a fleet can look healthy while dozens of jobs sit waiting. ## Schema Additive only: `0101_ci_queue_jobs.sql` (new table + `capacity` / `has_memory_cap` on `ci_runners`). ## After merge 1. Apply the SQL if not already applied. 2. Deploy web. 3. Roll the agent so the 60s reporter sends jobs.
Three parts: a layout that puts current work first, a fleet-wide event
channel, and live test results that actually arrive.

LAYOUT. Three zones, because the page answers three questions and only
two are urgent. IN FLIGHT gets cards, a status rail and a clock ticking
against the pipeline's median duration -- the rail stops at 94% and flips
to "over ~8:00" rather than implying a completion it cannot know. JUST
LANDED keeps cards and the outcome rail at a smaller scale. EARLIER is a
plain ledger, because that is the part you scan rather than look at.
Identity (app + PR, or the branch when there is no PR) is one shared
component across all three, so the same two fields lead everywhere and
only the type scale changes.

FLEET CHANNEL. Every existing broadcast is addressed to
`pipeline:<appId>`, which is right for an app's Pipeline tab and unusable
for /ci -- it would need a socket per app across 77 of them. Queued and
running Forgejo run progress now also goes to `ci:fleet`, stamped with
the app it came from so a fleet consumer can attribute it.

LIVE TEST RESULTS. The agent already tails the worktree's
.fg/check-events.ndjson and posts a folded summary every couple of
seconds, and that already fanned out on apps/web's `eventBus` -- whose
own comment says it only works "because ForgeGraph runs as a single
instance". That stopped being true when the control plane moved to
Cloudflare Workers: each request can land in a different isolate, so the
isolate receiving the agent POST is almost never the one holding a
browser's SSE connection, and live test results never arrived. They now
also go over the hub, which is a real cross-instance broker. The eventBus
publish stays for `fg watch`.

CONSUMPTION. useFleetCiStream subscribes to the one fleet channel;
LiveRefresh treats an event as a hint to look rather than as data, and
re-runs the server render so every number on the page moves together --
a partial live-patch would let the header disagree with the list. Bursts
are coalesced (test progress is ~2s per running build), a hidden tab
disconnects entirely and refreshes once on return, and polling takes over
whenever the hub is unreachable, so the page degrades to exactly its
previous behaviour rather than going silent.

The ci-progress route test stubbed a db that never modelled
resolveAppIdForChangeset's app query, so adding the broadcast broke it
with "rows.reduce is not a function". Mocked the hub boundary there
instead -- that route's subject is the metadata merge -- and added
coverage for the broadcast firing, for it being skipped when the
changeset resolves to no app, and for a terminal build broadcasting
nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DmzcuHid1zgyauBme4DSq6
feat(ci): runner cockpit — waiting queue, fleet rows, admin restart
Some checks failed
CI / gitleaks (pull_request) Successful in 8s
CI / ci (pull_request) Has been cancelled
CI / storybook (pull_request) Has been cancelled
4e891caf5b
Persist Forgejo queued/running jobs in ci_queue_jobs, derive idle vs busy,
and expose buildQueue.runners. Web /ci/runners and the mobile CI tab show
jobs waiting by label next to the runners that can claim them. queueStats
reads the snapshot, not builds. Admins can restart a mapped forgejo-runner
via hub exec.
gmackie force-pushed feat/ci-runner-cockpit from 4e891caf5b
Some checks failed
CI / gitleaks (pull_request) Successful in 8s
CI / ci (pull_request) Has been cancelled
CI / storybook (pull_request) Has been cancelled
to adf178a056
All checks were successful
CI / gitleaks (pull_request) Successful in 12s
CI / storybook (pull_request) Successful in 1m51s
forgegraph/ci CI passed
CI / ci (pull_request) Successful in 8m48s
2026-09-07 19:01:17 +00:00
Compare
gmackie force-pushed feat/ci-runner-cockpit from adf178a056
All checks were successful
CI / gitleaks (pull_request) Successful in 12s
CI / storybook (pull_request) Successful in 1m51s
forgegraph/ci CI passed
CI / ci (pull_request) Successful in 8m48s
to af27aff622
All checks were successful
CI / gitleaks (pull_request) Successful in 6s
CI / storybook (pull_request) Successful in 1m18s
forgegraph/ci CI passed
CI / ci (pull_request) Successful in 10m30s
2026-09-07 19:13:44 +00:00
Compare
gmackie scheduled this pull request to auto merge when all checks succeed 2026-09-07 19:17:13 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
gmackie/ForgeGraph!556
No description provided.