swarm-ui: a builds page for the swarm builder's runs, and the status links to it
/builds lists the builder's recent builds newest first, one card each with how it ended, and opens a failed build's nix tail. /builds?run=<id> opens that build's card and scrolls to it. The swarm-builder commit status now links there instead of to the raw JSON record.
This commit is contained in:
parent
3a7272bdfb
commit
0e77d61a9f
9 changed files with 391 additions and 13 deletions
|
|
@ -86,9 +86,10 @@ newest 100. Both are behind the swarm UI vhost's `admins` gate, and answer 503
|
||||||
when the controller has no swarm queue or can't open the bucket, 500 when it
|
when the controller has no swarm queue or can't open the bucket, 500 when it
|
||||||
can't read it.
|
can't read it.
|
||||||
|
|
||||||
The final status links to `https://<swarm domain>/api/builder/runs/<id>`, so
|
The final status links to `https://<swarm domain>/builds?run=<id>`, the swarm
|
||||||
selecting **Details** on a red `swarm-builder` check opens the record, with
|
UI's builds page, so selecting **Details** on a red `swarm-builder` check opens
|
||||||
`nix`'s tail, in the browser. A status has no link when the record wasn't
|
that build's card, with `nix`'s tail, in the browser. The raw record stays at
|
||||||
|
`GET /api/builder/runs/<id>`. A status has no link when the record wasn't
|
||||||
stored (the controller logs the failed write) or when the controller doesn't
|
stored (the controller logs the failed write) or when the controller doesn't
|
||||||
serve the swarm vhost (`SWARM_CONTROLLER_PUBLIC_URL` unset). The `pending`
|
serve the swarm vhost (`SWARM_CONTROLLER_PUBLIC_URL` unset). The `pending`
|
||||||
status has no link.
|
status has no link.
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ domain, covers host-level detail for one hive.
|
||||||
| `/schedules` | the scheduled prompts: add, edit, pause, resume and remove |
|
| `/schedules` | the scheduled prompts: add, edit, pause, resume and remove |
|
||||||
| `/issues` | a cross-repo issue report |
|
| `/issues` | a cross-repo issue report |
|
||||||
| `/review` | the review classifier: its recent runs, a try-out on a real PR, and its settings |
|
| `/review` | the review classifier: its recent runs, a try-out on a real PR, and its settings |
|
||||||
|
| `/builds` | the swarm builder's recent builds of agent config PRs, with what nix printed when one failed |
|
||||||
|
|
||||||
Everything it shows comes from [`swarm-controller`](../../swarm-controller/README.md).
|
Everything it shows comes from [`swarm-controller`](../../swarm-controller/README.md).
|
||||||
An agent created here or with `swarmctl agent create` starts `paused`; set it
|
An agent created here or with `swarmctl agent create` starts `paused`; set it
|
||||||
|
|
@ -259,6 +260,19 @@ live rows only.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
### Builds
|
||||||
|
|
||||||
|
The **builds** page lists the agent config PR heads the swarm builder built,
|
||||||
|
newest first, from `GET /api/builder/runs`. Each card shows the repo and PR,
|
||||||
|
the first seven characters of the commit, how the build ended (**built**,
|
||||||
|
**failed** or **error**), when it started and how long it took. Select a card to
|
||||||
|
read it: for a failed build, what `nix` printed; for a built one, the system
|
||||||
|
closure; for an error, why no build came back.
|
||||||
|
|
||||||
|
The **Details** link on a `swarm-builder` commit status opens this page as
|
||||||
|
`/builds?run=<id>`, with that build's card open and in view. The builder keeps
|
||||||
|
its newest 100 builds, so a link to an older one shows a note instead.
|
||||||
|
|
||||||
### Inference settings
|
### Inference settings
|
||||||
|
|
||||||
The agent's detail panel shows its model and effort under **inference**, each
|
The agent's detail panel shows its model and effort under **inference**, each
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { Shell } from "./shell/Shell.js";
|
||||||
import { AgentsPage } from "./pages/agents/AgentsPage.js";
|
import { AgentsPage } from "./pages/agents/AgentsPage.js";
|
||||||
import { AgentTerminalPage } from "./pages/agents/AgentTerminalPage.js";
|
import { AgentTerminalPage } from "./pages/agents/AgentTerminalPage.js";
|
||||||
import { AlertsPage } from "./pages/AlertsPage.js";
|
import { AlertsPage } from "./pages/AlertsPage.js";
|
||||||
|
import { BuildsPage } from "./pages/builds/BuildsPage.js";
|
||||||
import { ComponentsPage } from "./pages/ComponentsPage.js";
|
import { ComponentsPage } from "./pages/ComponentsPage.js";
|
||||||
import { JobsPage } from "./pages/JobsPage.js";
|
import { JobsPage } from "./pages/JobsPage.js";
|
||||||
import { HivesPage } from "./pages/HivesPage.js";
|
import { HivesPage } from "./pages/HivesPage.js";
|
||||||
|
|
@ -37,6 +38,7 @@ export function App() {
|
||||||
/>
|
/>
|
||||||
<Route path="/jobs" component={JobsPage} />
|
<Route path="/jobs" component={JobsPage} />
|
||||||
<Route path="/schedules" component={SchedulesPage} />
|
<Route path="/schedules" component={SchedulesPage} />
|
||||||
|
<Route path="/builds" component={BuildsPage} />
|
||||||
<Route path="/issues" component={IssueReportPage} />
|
<Route path="/issues" component={IssueReportPage} />
|
||||||
<Route path="/review" component={ReviewPage} />
|
<Route path="/review" component={ReviewPage} />
|
||||||
<Route path="/components" component={ComponentsPage} />
|
<Route path="/components" component={ComponentsPage} />
|
||||||
|
|
|
||||||
56
frontend/packages/swarm-ui/src/pages/builds/BuildsPage.css
Normal file
56
frontend/packages/swarm-ui/src/pages/builds/BuildsPage.css
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
/* The build list: one card per build, the head row a button that opens it. */
|
||||||
|
.ui-builds-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75em;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
.ui-builds-head {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em 0.75em;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.ui-builds-head:focus-visible {
|
||||||
|
outline: 2px solid var(--purple);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.ui-builds-title {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
.ui-builds-meta {
|
||||||
|
color: var(--muted);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.ui-builds-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4em;
|
||||||
|
margin-top: 0.6em;
|
||||||
|
}
|
||||||
|
.ui-builds-text {
|
||||||
|
margin: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
/* What nix printed: kept as printed, scrolls sideways rather than wrapping. */
|
||||||
|
.ui-builds-tail {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.6em 0.75em;
|
||||||
|
max-height: 24em;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 0.4em;
|
||||||
|
background: color-mix(in srgb, var(--crust) 60%, transparent);
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
.ui-builds-empty {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
189
frontend/packages/swarm-ui/src/pages/builds/BuildsPage.tsx
Normal file
189
frontend/packages/swarm-ui/src/pages/builds/BuildsPage.tsx
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
// <BuildsPage> — the swarm builder's recent builds, newest first: which agent
|
||||||
|
// config PR heads it built, how each ended, and what `nix` printed when one
|
||||||
|
// failed. One card per build; selecting a card opens it. Reads
|
||||||
|
// `GET /api/builder/runs`. A `swarm-builder` commit status links here as
|
||||||
|
// `/builds?run=<id>`, which opens that build's card and scrolls to it.
|
||||||
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
|
import { useSearch } from "wouter-preact";
|
||||||
|
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
||||||
|
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
||||||
|
import { Badge } from "@hive/shared/badge.js";
|
||||||
|
import { Card } from "../../ui/card/Card.js";
|
||||||
|
import { Panel } from "../../ui/panel/Panel.js";
|
||||||
|
import {
|
||||||
|
RefreshIntervalPicker,
|
||||||
|
useRefreshInterval,
|
||||||
|
type RefreshIntervalMs,
|
||||||
|
} from "../../ui/refresh-interval/RefreshInterval.js";
|
||||||
|
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||||
|
import { durationLabel } from "../review-runs/RunsTypes.js";
|
||||||
|
import {
|
||||||
|
OUTCOME_LABEL,
|
||||||
|
OUTCOME_TONE,
|
||||||
|
bodyText,
|
||||||
|
wantedRun,
|
||||||
|
type BuildRun,
|
||||||
|
} from "./builds-types.js";
|
||||||
|
import "./BuildsPage.css";
|
||||||
|
|
||||||
|
const RUNS = "/api/builder/runs";
|
||||||
|
const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000;
|
||||||
|
|
||||||
|
function cardId(id: string): string {
|
||||||
|
return `build-run-${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BuildCard({
|
||||||
|
run,
|
||||||
|
open,
|
||||||
|
onToggle,
|
||||||
|
}: {
|
||||||
|
run: BuildRun;
|
||||||
|
open: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
}) {
|
||||||
|
const body = bodyText(run);
|
||||||
|
const bodyId = `${cardId(run.id)}-body`;
|
||||||
|
return (
|
||||||
|
<Card class="ui-builds-card">
|
||||||
|
<div id={cardId(run.id)}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ui-builds-head"
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-controls={bodyId}
|
||||||
|
onClick={onToggle}
|
||||||
|
>
|
||||||
|
<span class="ui-builds-title">
|
||||||
|
<strong>
|
||||||
|
{run.repo} #{run.pr}
|
||||||
|
</strong>
|
||||||
|
<code>{run.head_sha.slice(0, 7)}</code>
|
||||||
|
</span>
|
||||||
|
<Badge
|
||||||
|
tone={OUTCOME_TONE[run.outcome]}
|
||||||
|
value={OUTCOME_LABEL[run.outcome]}
|
||||||
|
/>
|
||||||
|
<span class="ui-builds-meta">
|
||||||
|
<RelativeTime epochMs={run.started_unix * 1000} /> · took{" "}
|
||||||
|
{durationLabel(run.started_unix, run.finished_unix)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{open ? (
|
||||||
|
<div class="ui-builds-body" id={bodyId}>
|
||||||
|
{body ? (
|
||||||
|
<>
|
||||||
|
<p class="ui-builds-meta">{body.label}</p>
|
||||||
|
{body.preformatted ? (
|
||||||
|
<pre class="ui-builds-tail">{body.text}</pre>
|
||||||
|
) : (
|
||||||
|
<p class="ui-builds-text">{body.text}</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p class="ui-builds-meta">
|
||||||
|
nothing more was recorded for this build.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BuildsPage() {
|
||||||
|
const wanted = wantedRun(useSearch());
|
||||||
|
const [intervalMs, setIntervalMs] =
|
||||||
|
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
|
||||||
|
const [runs, setRuns] = useState<BuildRun[] | null>(null);
|
||||||
|
const [error, setError] = useState<ProblemDetails | null>(null);
|
||||||
|
// Which cards are open. Survives a refresh, so a list that updates under
|
||||||
|
// you does not fold the card you are reading. A linked build starts open.
|
||||||
|
const [open, setOpen] = useState<ReadonlySet<string>>(
|
||||||
|
() => new Set(wanted ? [wanted] : []),
|
||||||
|
);
|
||||||
|
const scrolledTo = useRef<string | null>(null);
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(RUNS);
|
||||||
|
if (!res.ok) {
|
||||||
|
setError(await readApiError(res));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setRuns((await res.json()) as BuildRun[]);
|
||||||
|
setError(null);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
setError({ detail: String(e) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useRefreshInterval(intervalMs, () => void load());
|
||||||
|
|
||||||
|
// Scroll to the linked build once its card exists, not on every refresh.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!wanted || !runs || scrolledTo.current === wanted) return;
|
||||||
|
const el = document.getElementById(cardId(wanted));
|
||||||
|
if (!el) return;
|
||||||
|
scrolledTo.current = wanted;
|
||||||
|
setOpen((cur) => new Set(cur).add(wanted));
|
||||||
|
el.scrollIntoView({ block: "center" });
|
||||||
|
}, [wanted, runs]);
|
||||||
|
|
||||||
|
function toggle(id: string) {
|
||||||
|
setOpen((cur) => {
|
||||||
|
const next = new Set(cur);
|
||||||
|
if (!next.delete(id)) next.add(id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const missing =
|
||||||
|
wanted !== null && runs !== null && !runs.some((r) => r.id === wanted);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel
|
||||||
|
title="builds"
|
||||||
|
icon="🔨"
|
||||||
|
actions={
|
||||||
|
<RefreshIntervalPicker
|
||||||
|
id="builds-refresh"
|
||||||
|
value={intervalMs}
|
||||||
|
onChange={setIntervalMs}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
The agent config PR heads the swarm builder built, newest first: how
|
||||||
|
each ended, and what nix printed when one failed. Select a build to read
|
||||||
|
it.
|
||||||
|
</p>
|
||||||
|
{error ? (
|
||||||
|
<ApiErrorPanel
|
||||||
|
context="the builds could not be loaded"
|
||||||
|
problem={error}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{missing ? (
|
||||||
|
<p class="ui-builds-meta" role="status">
|
||||||
|
the build in this link is no longer listed: the builder keeps only its
|
||||||
|
newest builds.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{runs !== null && runs.length === 0 && !error ? (
|
||||||
|
<p class="ui-builds-empty">No builds yet.</p>
|
||||||
|
) : null}
|
||||||
|
<div class="ui-builds-list">
|
||||||
|
{(runs ?? []).map((run) => (
|
||||||
|
<BuildCard
|
||||||
|
key={run.id}
|
||||||
|
run={run}
|
||||||
|
open={open.has(run.id)}
|
||||||
|
onToggle={() => toggle(run.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
frontend/packages/swarm-ui/src/pages/builds/builds-types.ts
Normal file
70
frontend/packages/swarm-ui/src/pages/builds/builds-types.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
// What the builds page decides without a DOM: the wire shape of one builder
|
||||||
|
// run, how each outcome reads, and which run a `?run=` link names. Kept out of
|
||||||
|
// `BuildsPage.tsx` so it is plain functions and can be tested.
|
||||||
|
import type { BadgeTone } from "@hive/shared/badge.js";
|
||||||
|
|
||||||
|
/** How a build ended, as `swarm-controller`'s `BuildOutcome` spells it. */
|
||||||
|
export type BuildOutcome = "built" | "failed" | "error";
|
||||||
|
|
||||||
|
/** One finished build, `GET /api/builder/runs`. */
|
||||||
|
export interface BuildRun {
|
||||||
|
/** `<agent>-<first 12 of head_sha>-<started_unix>`. */
|
||||||
|
id: string;
|
||||||
|
/** `agent-configs/<agent>`. */
|
||||||
|
repo: string;
|
||||||
|
pr: number;
|
||||||
|
head_sha: string;
|
||||||
|
started_unix: number;
|
||||||
|
finished_unix: number;
|
||||||
|
outcome: BuildOutcome;
|
||||||
|
/** The system closure's store path, for `built`. */
|
||||||
|
out_path?: string;
|
||||||
|
/** The last lines `nix` printed, for `failed`. */
|
||||||
|
error_tail?: string;
|
||||||
|
/** Why no build ran or came back, for `error`. */
|
||||||
|
detail?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OUTCOME_LABEL: Record<BuildOutcome, string> = {
|
||||||
|
built: "built",
|
||||||
|
failed: "failed",
|
||||||
|
error: "error",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OUTCOME_TONE: Record<BuildOutcome, BadgeTone> = {
|
||||||
|
built: "positive",
|
||||||
|
failed: "negative",
|
||||||
|
error: "warning",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The run a `?run=<id>` query names, or `null` when it names none. */
|
||||||
|
export function wantedRun(search: string): string | null {
|
||||||
|
const id = new URLSearchParams(search).get("run");
|
||||||
|
return id === null || id === "" ? null : id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What a run's card shows under its head: the text for its outcome. */
|
||||||
|
export function bodyText(run: BuildRun): {
|
||||||
|
label: string;
|
||||||
|
text: string;
|
||||||
|
preformatted: boolean;
|
||||||
|
} | null {
|
||||||
|
if (run.outcome === "failed" && run.error_tail) {
|
||||||
|
return {
|
||||||
|
label: "what nix printed",
|
||||||
|
text: run.error_tail,
|
||||||
|
preformatted: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (run.outcome === "built" && run.out_path) {
|
||||||
|
return { label: "system closure", text: run.out_path, preformatted: false };
|
||||||
|
}
|
||||||
|
if (run.outcome === "error" && run.detail) {
|
||||||
|
return {
|
||||||
|
label: "why no build came back",
|
||||||
|
text: run.detail,
|
||||||
|
preformatted: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
@ -56,6 +56,7 @@ const NAV_ITEMS: { href: string; label: string; accent: string }[] = [
|
||||||
{ href: "/schedules", label: "schedules", accent: "var(--cyan)" },
|
{ href: "/schedules", label: "schedules", accent: "var(--cyan)" },
|
||||||
{ href: "/issues", label: "issues", accent: "var(--yellow)" },
|
{ href: "/issues", label: "issues", accent: "var(--yellow)" },
|
||||||
{ href: "/review", label: "review", accent: "var(--amber)" },
|
{ href: "/review", label: "review", accent: "var(--amber)" },
|
||||||
|
{ href: "/builds", label: "builds", accent: "var(--green)" },
|
||||||
{ href: "/components", label: "components", accent: "var(--blue)" },
|
{ href: "/components", label: "components", accent: "var(--blue)" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
51
frontend/packages/swarm-ui/test/builds-types.test.mjs
Normal file
51
frontend/packages/swarm-ui/test/builds-types.test.mjs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
// The builds page's `?run=` link and what each outcome's card shows. Run by
|
||||||
|
// `npm test`, which Node runs with its built-in test runner and type
|
||||||
|
// stripping, so the `.ts` module is imported as it is.
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { test } from "node:test";
|
||||||
|
import { bodyText, wantedRun } from "../src/pages/builds/builds-types.ts";
|
||||||
|
|
||||||
|
const base = {
|
||||||
|
id: "atlas-abc-1",
|
||||||
|
repo: "agent-configs/atlas",
|
||||||
|
pr: 3,
|
||||||
|
head_sha: "abcdef0123456789",
|
||||||
|
started_unix: 100,
|
||||||
|
finished_unix: 160,
|
||||||
|
};
|
||||||
|
|
||||||
|
test("a ?run= link names the run it carries", () => {
|
||||||
|
assert.equal(wantedRun("?run=atlas-abc-1"), "atlas-abc-1");
|
||||||
|
assert.equal(wantedRun("run=atlas-abc-1&x=1"), "atlas-abc-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no run, or an empty one, names none", () => {
|
||||||
|
assert.equal(wantedRun(""), null);
|
||||||
|
assert.equal(wantedRun("?x=1"), null);
|
||||||
|
assert.equal(wantedRun("?run="), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a failed build shows what nix printed, as it was printed", () => {
|
||||||
|
const body = bodyText({ ...base, outcome: "failed", error_tail: "error: x" });
|
||||||
|
assert.deepEqual(body, {
|
||||||
|
label: "what nix printed",
|
||||||
|
text: "error: x",
|
||||||
|
preformatted: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a built one shows its closure, an errored one its reason", () => {
|
||||||
|
assert.equal(
|
||||||
|
bodyText({ ...base, outcome: "built", out_path: "/nix/store/x" })?.text,
|
||||||
|
"/nix/store/x",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
bodyText({ ...base, outcome: "error", detail: "no node" })?.text,
|
||||||
|
"no node",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a run with no text for its outcome shows no body", () => {
|
||||||
|
assert.equal(bodyText({ ...base, outcome: "failed" }), null);
|
||||||
|
assert.equal(bodyText({ ...base, outcome: "built", error_tail: "x" }), null);
|
||||||
|
});
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! The `BuildAgent` node: send a config PR's head to the swarm builder over the
|
//! The `BuildAgent` node: send a config PR's head to the swarm builder over the
|
||||||
//! queue, turn its answer into the [`CONTEXT`] commit status on that head, and
|
//! queue, turn its answer into the [`CONTEXT`] commit status on that head, and
|
||||||
//! record the build, `nix`'s tail with it, in [`runs`], whose
|
//! record the build, `nix`'s tail with it, in [`runs`]. The status links to the
|
||||||
//! `GET /api/builder/runs/{id}` the status links to.
|
//! swarm UI's builds page for that build.
|
||||||
//!
|
//!
|
||||||
//! Nothing is pushed, and the closure the builder made stays in its host's
|
//! Nothing is pushed, and the closure the builder made stays in its host's
|
||||||
//! store. A red status blocks nothing; the merge deploys as before.
|
//! store. A red status blocks nothing; the merge deploys as before.
|
||||||
|
|
@ -209,13 +209,7 @@ async fn record(
|
||||||
if !builds.record(&build).await {
|
if !builds.record(&build).await {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
link_base.map(|base| {
|
link_base.map(|base| format!("{}/builds?run={}", base.trim_end_matches('/'), build.id))
|
||||||
format!(
|
|
||||||
"{}/api/builder/runs/{}",
|
|
||||||
base.trim_end_matches('/'),
|
|
||||||
build.id
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A failed node carrying `error`, and the status write's own failure when
|
/// A failed node carrying `error`, and the status write's own failure when
|
||||||
|
|
@ -354,7 +348,7 @@ mod tests {
|
||||||
assert_eq!((&build.out_path, &build.detail), (&None, &None));
|
assert_eq!((&build.out_path, &build.detail), (&None, &None));
|
||||||
assert!(build.id.starts_with("atlas-abc-"), "{}", build.id);
|
assert!(build.id.starts_with("atlas-abc-"), "{}", build.id);
|
||||||
assert!(build.finished_unix >= build.started_unix);
|
assert!(build.finished_unix >= build.started_unix);
|
||||||
let link = format!("https://swarm.example/api/builder/runs/{}", build.id);
|
let link = format!("https://swarm.example/builds?run={}", build.id);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
written,
|
written,
|
||||||
[
|
[
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue