Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 1 | import { env } from "cloudflare:workers"; |
| 2 | ||
| 3 | import { | |
| 4 | type RegistrationToken, | |
| 5 | type Runner, | |
| 6 | type RunnerGroup, | |
| 7 | type RunnerSettings, | |
| 8 | type RunnersOwner, | |
| 9 | type User, | |
| The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how. | 10 | RUNNER_ACTIVITY_LIMIT, |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 11 | runnersClient, |
| 12 | } from "@g1t/contracts"; | |
| 13 | ||
| 14 | import { instrumented } from "./perf.server"; | |
| The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how. | 15 | import { type CloudNow, type CloudPrice, type OwnWaiting, type RunnerCost, cloudNow, cloudPrices, ownWaiting, runnerCost } from "./runners"; |
| 16 | import { agents, billing } from "./services.server"; | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 17 | |
| 18 | /** Self-hosted runners, kept by the actions service. */ | |
| 19 | export const runners = runnersClient(instrumented("actions", env.ACTIONS)); | |
| 20 | ||
| 21 | export type RunnersData = { | |
| 22 | runners: Runner[]; | |
| 23 | /** A workspace's groups; empty for a repository. */ | |
| 24 | groups: RunnerGroup[]; | |
| 25 | settings: RunnerSettings | null; | |
| 26 | /** The workspace's repositories, for choosing a group's. */ | |
| 27 | repositories: string[]; | |
| 28 | error: string | null; | |
| 29 | }; | |
| 30 | ||
| 31 | export async function loadRunners(owner: RunnersOwner, actor: User, repositories: string[] = []): Promise<RunnersData> { | |
| 32 | const [list, groups, settings] = await Promise.all([ | |
| 33 | runners.list(actor, owner), | |
| 34 | "workspace" in owner ? runners.groups(actor, owner.workspace) : Promise.resolve(null), | |
| 35 | runners.settings(actor, owner), | |
| 36 | ]); | |
| 37 | return { | |
| 38 | runners: list.ok ? list.value : [], | |
| 39 | groups: groups?.ok ? groups.value : [], | |
| 40 | settings: settings.ok ? settings.value : null, | |
| 41 | repositories, | |
| 42 | error: list.ok ? null : list.error.message, | |
| 43 | }; | |
| 44 | } | |
| 45 | ||
| The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how. | 46 | /** The workspace's Runners page: its runners, and g1t's cloud beside them. */ |
| 47 | export type RunnersPageData = RunnersData & { | |
| 48 | /** What runs on g1t's cloud now; null when neither the work nor the actions service answered. */ | |
| 49 | cloud: CloudNow | null; | |
| 50 | /** What waits for the workspace's own runners; null when the actions service did not answer. */ | |
| 51 | waiting: OwnWaiting | null; | |
| 52 | /** This month's machine time; null when billing did not answer. */ | |
| 53 | cost: RunnerCost | null; | |
| 54 | /** A minute on g1t's cloud, from the price book; null when it has none. */ | |
| 55 | prices: CloudPrice[] | null; | |
| 56 | /** Agent runs at once on g1t's cloud the plan allows; null for no cap, or unknown. */ | |
| 57 | agentCap: number | null; | |
| 58 | }; | |
| 59 | ||
| 60 | /** Active agent runs read: the work service's most in one list. */ | |
| 61 | const RUN_PAGE = 200; | |
| 62 | ||
| 63 | const warn = (what: string) => (error: unknown) => { | |
| 64 | console.warn(`runners: ${what} failed`, error); | |
| 65 | return null; | |
| 66 | }; | |
| 67 | ||
| 68 | /** | |
| 69 | * Every read the workspace's Runners page makes, at once. The runners, | |
| 70 | * groups and settings come as on a project's page; what runs on g1t's | |
| 71 | * cloud, what waits, and this month's time each come from their own | |
| 72 | * service, and one that does not answer leaves only its part saying so. | |
| 73 | */ | |
| 74 | export async function loadRunnersPage(workspace: string, actor: User, repositories: string[] = [], now = new Date()): Promise<RunnersPageData> { | |
| 75 | const from = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString().slice(0, 10); | |
| 76 | const until = now.toISOString().slice(0, 10); | |
| 77 | const [base, activity, runs, report, book, entitlements] = await Promise.all([ | |
| 78 | loadRunners({ workspace }, actor, repositories), | |
| 79 | runners | |
| 80 | .activity(actor, workspace) | |
| 81 | .then((result) => (result.ok ? result.value : null)) | |
| 82 | .catch(warn("runner activity")), | |
| 83 | agents | |
| 84 | .listRuns(actor, { workspace, active: true, limit: RUN_PAGE }) | |
| 85 | .then((result) => (result.ok ? result.value : null)) | |
| 86 | .catch(warn("agent runs")), | |
| 87 | billing | |
| 88 | .usageReport(workspace, actor, { from, until }) | |
| 89 | .then((result) => (result.ok ? result.value : null)) | |
| 90 | .catch(warn("usage report")), | |
| 91 | billing.prices().catch(warn("prices")), | |
| 92 | billing.entitlements(workspace).catch(warn("entitlements")), | |
| 93 | ]); | |
| 94 | return { | |
| 95 | ...base, | |
| 96 | cloud: cloudNow(runs, activity, RUN_PAGE, RUNNER_ACTIVITY_LIMIT), | |
| 97 | waiting: ownWaiting(activity), | |
| 98 | cost: runnerCost(report), | |
| 99 | prices: cloudPrices(book?.prices ?? null), | |
| 100 | agentCap: entitlements && entitlements.maxConcurrentAgents > 0 ? entitlements.maxConcurrentAgents : null, | |
| 101 | }; | |
| 102 | } | |
| 103 | ||
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 104 | export type RunnersAction = { |
| 105 | error?: string; | |
| 106 | notice?: string; | |
| 107 | /** A registration token just made: shown once, in the install steps. */ | |
| 108 | token?: RegistrationToken; | |
| 109 | }; | |
| 110 | ||
| 111 | /** Every form on the Runners page. */ | |
| 112 | export async function actOnRunners(owner: RunnersOwner, actor: User, form: FormData): Promise<RunnersAction> { | |
| 113 | const intent = String(form.get("intent") ?? ""); | |
| 114 | const workspace = "workspace" in owner ? owner.workspace : owner.repo.namespace.toLowerCase(); | |
| 115 | const list = (name: string) => | |
| 116 | String(form.get(name) ?? "") | |
| 117 | .split(/[\s,]+/) | |
| 118 | .map((item) => item.trim()) | |
| 119 | .filter(Boolean); | |
| 120 | switch (intent) { | |
| 121 | case "token": { | |
| 122 | const made = await runners.createToken(actor, owner, String(form.get("group") ?? "") || undefined); | |
| 123 | return made.ok ? { token: made.value } : { error: made.error.message }; | |
| 124 | } | |
| 125 | case "remove": { | |
| 126 | const removed = await runners.remove(actor, owner, String(form.get("id") ?? "")); | |
| 127 | return removed.ok ? { notice: `Removed ${String(form.get("name") ?? "the runner")}.` } : { error: removed.error.message }; | |
| 128 | } | |
| 129 | case "group": { | |
| 130 | const id = String(form.get("id") ?? "") || undefined; | |
| 131 | const repositories = form.get("reach") === "some" ? form.getAll("repository").map(String) : []; | |
| 132 | if (form.get("reach") === "some" && repositories.length === 0) return { error: "Choose at least one repository, or every repository." }; | |
| 133 | const saved = await runners.setGroup(actor, workspace, { | |
| 134 | id, | |
| 135 | name: String(form.get("name") ?? "").trim() || undefined, | |
| 136 | repositories, | |
| 137 | }); | |
| 138 | return saved.ok ? { notice: `Saved the group ${saved.value.name}.` } : { error: saved.error.message }; | |
| 139 | } | |
| 140 | case "delete-group": { | |
| 141 | const deleted = await runners.deleteGroup(actor, workspace, String(form.get("id") ?? "")); | |
| 142 | return deleted.ok ? { notice: "Deleted the group; its runners joined the default group." } : { error: deleted.error.message }; | |
| 143 | } | |
| 144 | case "settings": { | |
| 145 | const saved = await runners.setSettings(actor, owner, { | |
| 146 | agents_on_self_hosted: form.get("agents") === "on", | |
| 147 | agent_labels: list("agentLabels"), | |
| 148 | fork_pull_requests: form.get("forks") === "on", | |
| 149 | }); | |
| 150 | return saved.ok ? { notice: "Saved." } : { error: saved.error.message }; | |
| 151 | } | |
| 152 | case "inherit": { | |
| 153 | const saved = await runners.setSettings(actor, owner, { inherit: true }); | |
| 154 | return saved.ok ? { notice: "This project follows the workspace's runner settings again." } : { error: saved.error.message }; | |
| 155 | } | |
| 156 | default: | |
| 157 | return { error: "Nothing to do." }; | |
| 158 | } | |
| 159 | } |