Hive Hive
Sign in

feat(server): Tuist Runners dashboard + billing — runners landing, workflows, jobs, settings hub

GitHub issue · Closed

Metadata
Source
tuist/tuist #10848
Updated
Jun 24, 2026
Domains
Atlas Compute
Details

Summary

Builds out the Tuist Runners customer-facing surface — landing page → workflows list → workflow detail → jobs list → job detail — and the billing layer that anchors metered compute behind it. Also reorganizes the account navigation so the new pages have somewhere to live without each subsystem doubling the tab count.

96 files, +12.8k/-1.1k. Squashes a flat horizontal-tab account header into a sidebar + nested Settings hub, adds five new LiveViews, a new Postgres billing table, and a handful of ClickHouse migrations on the existing runner_jobs table.

Customer-facing surfaces

Navigation

  • Account layout: leading sidebar matching the project layout (Projects, Runners, Members, Settings).
  • Settings hub: Integrations, Billing, Authentication move under /settings/* as a horizontal tab bar. Stripe success/manage redirects and the GitHub App post-install redirect updated to match.

Runners overview — /:account/runners

  • Total jobs, p50/p90/p99 job duration, p50/p90/p99 workflow duration widgets with trend chips and a date-range picker.
  • Recent jobs + Recent workflow runs cards (bar charts + tables), each row clickable to the relevant detail page.
  • Page-level Platform (macOS / Linux) + Repository dropdowns scope every widget.

Workflows list — /:account/runners/workflows

  • Per-workflow rollups (one row per (workflow_name, repository)): total jobs, success rate, avg duration, last run.
  • Column-header sort (workflow / jobs / success_rate / avg_duration), workflow-name search, Repository / Platform dropdowns, pagination.

Workflow detail — /:account/runners/workflows/:owner/:repo/:workflow_name

  • Scoped widgets (total jobs, failed jobs, p50/p90/p99 duration, queue time) and a jobs table for that workflow.
  • Header carries the repo as a clickable GitHub link.

Jobs list — /:account/runners/jobs

  • One row per workflow_job, status pill filter (queued/claimed/running/completed), Repository / Platform / Workflow / Job / Branch / Conclusion filter chips, free-text job-name search, column-header sort, pagination.
  • Analytics widgets above the table: Job runs (Total / Passed / Failed dropdown), Cumulative compute minutes, p50/p90/p99 job duration, p50/p90/p99 queue time. Duration charts toggle between line and scatter; the scatter groups by status or platform via a Group-by dropdown.
  • Charts switch from daily to hourly buckets for windows ≤ 36h.
  • Running / Queued widgets live-update over PubSub on every state-transition INSERT.

Job detail — /:account/runners/runs/:workflow_run_id/jobs/:workflow_job_id

  • URL hierarchy mirrors GitHub’s /<owner>/<repo>/actions/runs/<run_id>/job/<job_id> so the two stay symmetric; mismatched run_id in the URL 404s the same way GitHub does.
  • Header card with status + GitHub deep-link; metadata grid (repository, workflow, job, branch, commit, fleet/platform, runner, run attempt, IDs).
  • Timeline card: queue time / claim → start / runtime / total durations + relative + absolute timestamps per lifecycle event.

What’s behind the pages

Billing

  • New Postgres runner_sessions table (one row per Pod we provisioned), keyed off the Pod’s wall-clock lifetime — the signal Namespace and Blacksmith bill against. runner_claims is deleted on completion so can’t drive historical invoicing; runner_jobs (CH) tracks the GitHub-side workflow_job lifecycle which doesn’t match what we charge for.
  • Tuist.Runners.Billing.compute_minutes/2 rolls sessions up to total_ms + per-bucket series + trend-vs-previous-window, scoped by the same Repository / Workflow / Platform opts the widgets use.
  • Sessions are opened at claim-win (Jobs.record_claimed/3RunnerSessions.open/1) and closed by the runners-controller via POST /api/internal/runners/pods/stopped when it observes the Pod’s container terminate — the close signal is K8s’s containerStatuses[runner].state.terminated.finishedAt, not the completion webhook.
  • pods/stopped is gated by SA-principal check: TokenReview validates the bearer token, then we compare the returned (namespace, name) against the rendered runners-controller SA. Any other in-cluster SA gets 401.

Schema

  • runner_jobs.workflow_name String DEFAULT '' (added on this branch). Plain String, not LowCardinality — workflow_name is per-account free-form input and would outgrow LowCardinality’s sweet spot.
  • runner_jobs.reporepository rename to align with the rest of the codebase’s repository convention (URL params, LiveView assigns, GitHub webhook payloads, OIDC claims).
  • runner_jobs.{claimed_at,started_at,completed_at} switched from non-null DateTime64 with an epoch sentinel to Nullable(DateTime64). “Not yet” is NULL instead of an epoch literal that occasionally leaked into the UI as 1970-01-01. All read paths swap toUnixTimestamp64Milli(?) > 0 guards for isNotNull(?).
  • runner_sessions carries denormalized repository + workflow_name so the Compute Minutes widget’s repository / workflow_name filters don’t have to join against ClickHouse.

Analytics queries

  • Single latest_jobs_subquery/2 GROUP BY + argMax dedup pattern reused across every read path on runner_jobs — same logical view FROM … FINAL would produce without paying the per-read part-merge cost.
  • count_workflows_for_account/2 routes through the same subquery as list_workflows_for_account/2 so paged list / count never drift.
  • workflows_duration rolls up via GROUP BY workflow_run_id + HAVING countIf(status != 'completed') == 0 — a run with one done + one queued job no longer contributes a truncated duration sample. minIf/maxIf aggregates with isNotNull guards exclude skipped/cancelled-and-never-started jobs from the timestamp bounds.

Web layer

  • Tuist.Utilities.DateFormatter gains nil/missing handling — all fallback strings ("None", "Unknown") routed through dgettext("dashboard", …) so non-English viewers no longer see English fallbacks mixed into otherwise-localized tables.
  • Empty-state docs links on Runners / Workflows / Workflow detail / Jobs pages routed through TuistWeb.Marketing.Localization.localized_href/1 so a viewer on /ja/… lands on the Japanese docs.

Test plan

  • Server lib tests: mix test test/tuist/runners/ test/tuist_web/controllers/runner_pods_controller_test.exs — 87/87 pass
  • LiveView tests: mix test test/tuist_web/live/runner_*_live_test.exs test/tuist_web/live/runners_live_test.exs — all pass
  • DateFormatter tests survive the localization swap: mix test test/tuist_web/utilities/date_formatter_test.exs — pass (gettext returns the msgid when no translation exists)
  • mix compile --warnings-as-errors + mix format clean
  • Browser-verified locally across all five LiveViews with seeded fixtures: list / filter / column-sort / pagination / detail row click-through / Settings tab switching / sidebar highlight / line+scatter chart toggle / date-range picker / Group-by dropdown
  • Staging deploy (run 26502770968) for end-to-end runners verification with the migrations applied

🤖 Generated with Claude Code

Comments
F
fortmarek May 25, 2026

Here’s some screenshot from this feature:

F
fortmarek May 25, 2026

As a follow-up, I will work on the job detail itself, so it includes things like logs, steps, and machine metrics.

TA
tuist-atlas[bot] May 28, 2026

The Tuist Runners dashboard and billing features (including runners landing, workflows, jobs, and settings hub) are now available in xcresult-processor-image@0.8.0. Update to this version to use these features.