@fungi.computer/bonsai 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -0
- package/README.md +66 -12
- package/backend/build-workflow.ts +137 -0
- package/backend/index.ts +13 -7
- package/backend/methods.ts +222 -19
- package/package.json +54 -18
- package/scripts/release.mjs +12 -1
- package/scripts/shed-terminal-proof.mjs +30 -28
- package/shared/contract.ts +41 -0
- package/test/apps-build-install.test.ts +399 -26
- package/test/browser/build-workflow.browser.mjs +104 -0
- package/test/browser/repository-browser.fixture.mjs +84 -0
- package/test/browser/repository-settings-fixture.tsx +198 -2
- package/test/browser/repository-settings.browser.mjs +188 -26
- package/test/checks-workflow-template.test.mjs +12 -10
- package/test/favicon.test.mjs +25 -0
- package/test/git-push.native.mjs +30 -0
- package/test/git-push.worker.ts +51 -3
- package/test/merge-template-build.test.mjs +44 -201
- package/test/present.test.ts +27 -0
- package/test/storyboard-save-https-git.fixture.mjs +35 -29
- package/test/storyboard-save-template.test.mjs +61 -12
- package/vite.config.ts +19 -1
- package/vitest.config.ts +43 -1
- package/web/app-build-panel.tsx +183 -95
- package/web/bonsai.css +1 -0
- package/web/change-page.tsx +10 -6
- package/web/index.html +0 -1
- package/web/repo-settings.tsx +1 -1
- package/web/public/favicon.svg +0 -1
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { Buffer } from "node:buffer";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import { execFile } from "node:child_process";
|
|
3
|
+
import { execFile, spawn } from "node:child_process";
|
|
5
4
|
import { dirname, join } from "node:path";
|
|
6
5
|
import { DatabaseSync } from "node:sqlite";
|
|
7
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
mkdir,
|
|
8
|
+
mkdtemp,
|
|
9
|
+
readFile,
|
|
10
|
+
readdir,
|
|
11
|
+
rm,
|
|
12
|
+
writeFile,
|
|
13
|
+
} from "node:fs/promises";
|
|
8
14
|
import { tmpdir } from "node:os";
|
|
9
15
|
import process from "node:process";
|
|
10
16
|
import { test } from "node:test";
|
|
@@ -24,7 +30,7 @@ import {
|
|
|
24
30
|
// Shed's, Prints', Inkcap's and Bonsai's sources are pinned at runtime only;
|
|
25
31
|
// Bonsai's compiler options do not own them.
|
|
26
32
|
import { buildAppApi } from "@fungi.computer/agar/backend-declaration";
|
|
27
|
-
import { mintComputerProcessId } from "@fungi.computer/computer
|
|
33
|
+
import { mintComputerProcessId } from "@fungi.computer/computer-contract";
|
|
28
34
|
import { appBackend as bonsaiBackend } from "../backend/methods.ts";
|
|
29
35
|
import { appBackend as shedBackend } from "../../shed/backend/methods.ts";
|
|
30
36
|
import { createShedRuns } from "../../shed/backend/runs.ts";
|
|
@@ -37,6 +43,50 @@ import {
|
|
|
37
43
|
|
|
38
44
|
const execFileAsync = promisify(execFile);
|
|
39
45
|
|
|
46
|
+
for (const startupFails of [false, true]) {
|
|
47
|
+
test(`HTTPS Git fixture removes certificate files after ${startupFails ? "startup failure" : "server close"}`, async (t) => {
|
|
48
|
+
const work = await mkdtemp(
|
|
49
|
+
join(tmpdir(), "storyboard-save-fixture-check-"),
|
|
50
|
+
);
|
|
51
|
+
t.after(() => rm(work, { recursive: true, force: true }));
|
|
52
|
+
const scratch = join(work, "tmp");
|
|
53
|
+
await mkdir(scratch);
|
|
54
|
+
const fixture = new URL(
|
|
55
|
+
"./storyboard-save-https-git.fixture.mjs",
|
|
56
|
+
import.meta.url,
|
|
57
|
+
);
|
|
58
|
+
const env = {
|
|
59
|
+
...process.env,
|
|
60
|
+
HOME: work,
|
|
61
|
+
TMPDIR: scratch,
|
|
62
|
+
TMP: scratch,
|
|
63
|
+
TEMP: scratch,
|
|
64
|
+
};
|
|
65
|
+
if (startupFails) env.OPENSSL_CONF = join(work, "missing-openssl.cnf");
|
|
66
|
+
// tmpdir() is process-local. Give the real producer its own environment,
|
|
67
|
+
// rather than changing the test runner's environment or mocking openssl.
|
|
68
|
+
await execFileAsync(
|
|
69
|
+
process.execPath,
|
|
70
|
+
[
|
|
71
|
+
"--input-type=module",
|
|
72
|
+
"--eval",
|
|
73
|
+
`
|
|
74
|
+
import assert from "node:assert/strict";
|
|
75
|
+
import { startHttpsGitServer } from ${JSON.stringify(fixture.href)};
|
|
76
|
+
const startup = startHttpsGitServer(${JSON.stringify(work)});
|
|
77
|
+
${
|
|
78
|
+
startupFails
|
|
79
|
+
? "await assert.rejects(startup, { status: 1 });"
|
|
80
|
+
: "const server = await startup; await server.close();"
|
|
81
|
+
}
|
|
82
|
+
`,
|
|
83
|
+
],
|
|
84
|
+
{ env },
|
|
85
|
+
);
|
|
86
|
+
assert.deepEqual(await readdir(scratch), []);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
40
90
|
async function readTemplate(name) {
|
|
41
91
|
const template = z
|
|
42
92
|
.object({ definition: z.json() })
|
|
@@ -192,7 +242,13 @@ test("the Save to git flow runs through Shed's real run handler and Inkcap's rea
|
|
|
192
242
|
procs.set(id, record);
|
|
193
243
|
const child = spawn("bash", ["-c", localCommand], {
|
|
194
244
|
cwd: cwd ? localize(cwd) : undefined,
|
|
195
|
-
|
|
245
|
+
// The trusted host supplies the Git remote after admission; Shed
|
|
246
|
+
// and its command carry no credential.
|
|
247
|
+
env: {
|
|
248
|
+
...process.env,
|
|
249
|
+
GIT_SSL_NO_VERIFY: "true",
|
|
250
|
+
FUNGI_GIT_REMOTE: remote,
|
|
251
|
+
},
|
|
196
252
|
});
|
|
197
253
|
let stderr = "";
|
|
198
254
|
child.stderr.on("data", (chunk) => {
|
|
@@ -209,13 +265,6 @@ test("the Save to git flow runs through Shed's real run handler and Inkcap's rea
|
|
|
209
265
|
processes() {
|
|
210
266
|
return Promise.resolve({ processes: [...procs.values()] });
|
|
211
267
|
},
|
|
212
|
-
workflowGitPass() {
|
|
213
|
-
return Promise.resolve({
|
|
214
|
-
passId: "pass_test",
|
|
215
|
-
remote,
|
|
216
|
-
expiresAt: Date.now() + 300000,
|
|
217
|
-
});
|
|
218
|
-
},
|
|
219
268
|
async write({ path, content }) {
|
|
220
269
|
const target = localize(path);
|
|
221
270
|
await mkdir(dirname(target), { recursive: true });
|
package/vite.config.ts
CHANGED
|
@@ -2,12 +2,30 @@ import { fileURLToPath } from "node:url";
|
|
|
2
2
|
import tailwindcss from "@tailwindcss/vite";
|
|
3
3
|
import react from "@vitejs/plugin-react";
|
|
4
4
|
import { defineConfig } from "vite";
|
|
5
|
+
import { fungiFaviconHref } from "@fungi.computer/sigils/favicon";
|
|
5
6
|
|
|
6
7
|
export default defineConfig({
|
|
7
8
|
envDir: false,
|
|
8
9
|
base: "./",
|
|
9
10
|
root: fileURLToPath(new URL("./web", import.meta.url)),
|
|
10
|
-
plugins: [
|
|
11
|
+
plugins: [
|
|
12
|
+
react(),
|
|
13
|
+
tailwindcss(),
|
|
14
|
+
{
|
|
15
|
+
name: "fungi-favicon",
|
|
16
|
+
transformIndexHtml: () => [
|
|
17
|
+
{
|
|
18
|
+
tag: "link",
|
|
19
|
+
attrs: {
|
|
20
|
+
rel: "icon",
|
|
21
|
+
type: "image/svg+xml",
|
|
22
|
+
href: fungiFaviconHref,
|
|
23
|
+
},
|
|
24
|
+
injectTo: "head",
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
],
|
|
11
29
|
build: {
|
|
12
30
|
target: "es2022",
|
|
13
31
|
outDir: fileURLToPath(new URL("./.botanical/web", import.meta.url)),
|
package/vitest.config.ts
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
import { defineConfig } from "vitest/config";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* These files drive real `git` subprocesses (init, http-backend pushes, clones,
|
|
5
|
+
* plumbing reads) through Agar's real-Git test kit. Each Git spawn costs a few
|
|
6
|
+
* milliseconds idle and tens under CI load, so a test that is about 1 s on an
|
|
7
|
+
* idle box crosses the 5 s default when the gate runs at several times
|
|
8
|
+
* oversubscription. They get their own project and timeout; the fast unit
|
|
9
|
+
* tests keep Vitest's default.
|
|
10
|
+
*/
|
|
11
|
+
const realGit = [
|
|
12
|
+
"test/background-runtime.test.ts",
|
|
13
|
+
"test/changes.test.ts",
|
|
14
|
+
"test/file-edits.test.ts",
|
|
15
|
+
"test/files-read-copy-calls.test.ts",
|
|
16
|
+
"test/graph-commit-read-copy-calls.test.ts",
|
|
17
|
+
"test/merge-background.test.ts",
|
|
18
|
+
"test/merge-conflict.test.ts",
|
|
19
|
+
"test/merge-repro.test.ts",
|
|
20
|
+
"test/read-copy-composer.test.ts",
|
|
21
|
+
"test/read-copy-lifetimes.test.ts",
|
|
22
|
+
"test/repository-deletion.test.ts",
|
|
23
|
+
];
|
|
24
|
+
|
|
3
25
|
export default defineConfig({
|
|
4
|
-
test: {
|
|
26
|
+
test: {
|
|
27
|
+
projects: [
|
|
28
|
+
{
|
|
29
|
+
test: {
|
|
30
|
+
name: "unit",
|
|
31
|
+
include: ["test/**/*.test.ts"],
|
|
32
|
+
exclude: realGit,
|
|
33
|
+
environment: "node",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
test: {
|
|
38
|
+
name: "real-git",
|
|
39
|
+
include: realGit,
|
|
40
|
+
environment: "node",
|
|
41
|
+
testTimeout: 30_000,
|
|
42
|
+
hookTimeout: 30_000,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
5
47
|
});
|
package/web/app-build-panel.tsx
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
|
+
import { replayKey } from "@fungi.computer/cairn";
|
|
1
2
|
import { useState } from "react";
|
|
2
|
-
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import * as S from "effect/Schema";
|
|
3
5
|
import { z } from "zod";
|
|
6
|
+
import { workflowDefinitionSchema } from "@fungi.computer/braid";
|
|
7
|
+
import { runReply } from "@fungi.computer/workflows-wire";
|
|
8
|
+
import {
|
|
9
|
+
RunTimeline,
|
|
10
|
+
OutputError,
|
|
11
|
+
runStatusSchema,
|
|
12
|
+
runSteps,
|
|
13
|
+
terminal,
|
|
14
|
+
timelineItems,
|
|
15
|
+
} from "@fungi.computer/trellis/run-timeline";
|
|
16
|
+
import template from "@fungi.computer/trellis/templates/bonsai-build-on-merge" with { type: "json" };
|
|
4
17
|
import { Button } from "@fungi.computer/caps/components/button";
|
|
5
18
|
import {
|
|
6
19
|
Card,
|
|
@@ -10,57 +23,83 @@ import {
|
|
|
10
23
|
} from "@fungi.computer/caps/components/card";
|
|
11
24
|
import { Input } from "@fungi.computer/caps/components/input";
|
|
12
25
|
import { pendingSchema } from "@fungi.computer/agar";
|
|
13
|
-
import { appBuildView,
|
|
26
|
+
import { appBuildView, appRegistration } from "../shared/contract.js";
|
|
14
27
|
import { callMethod } from "./transport.js";
|
|
15
28
|
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
/** Start a build of a registered App's source, then install or switch its
|
|
22
|
-
* Release once it finishes. Owner only; the host re-checks that on every call. */
|
|
23
|
-
export function AppBuildPanel() {
|
|
24
|
-
const [appId, setAppId] = useState("");
|
|
25
|
-
const [sourceRevision, setSourceRevision] = useState("");
|
|
26
|
-
const [installationId, setInstallationId] = useState("");
|
|
27
|
-
const [jobId, setJobId] = useState<string | null>(null);
|
|
29
|
+
const buildStatus = z.union([pendingSchema, appBuildView]).nullable();
|
|
30
|
+
const definition = S.decodeUnknownSync(workflowDefinitionSchema)(
|
|
31
|
+
template.definition,
|
|
32
|
+
);
|
|
33
|
+
const appDisplay = { app_bonsai: { displayName: "Bonsai", icon: "bonsai" } };
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
/** Both this owner action and accepted main pushes start the same Workflow.
|
|
36
|
+
* Only its host-attested install step selects a Release; the panel never does. */
|
|
37
|
+
export function AppBuildPanel({ repo }: { repo: string }) {
|
|
38
|
+
const queryClient = useQueryClient();
|
|
39
|
+
const [appId, setAppId] = useState<string | null>(null);
|
|
40
|
+
const [sourceRevision, setSourceRevision] = useState<string | null>(null);
|
|
41
|
+
const [installationId, setInstallationId] = useState<string | null>(null);
|
|
42
|
+
const [runId, setRunId] = useState<string | null>(null);
|
|
43
|
+
const statusKey = ["apps.buildStatus", repo];
|
|
36
44
|
const status = useQuery({
|
|
37
|
-
queryKey:
|
|
45
|
+
queryKey: statusKey,
|
|
46
|
+
queryFn: () => callMethod("apps.buildStatus", { repo }, buildStatus),
|
|
47
|
+
refetchInterval: 5000,
|
|
48
|
+
});
|
|
49
|
+
const settled =
|
|
50
|
+
status.data == null || "kind" in status.data ? null : status.data;
|
|
51
|
+
const selectedAppId = appId ?? settled?.appId ?? "";
|
|
52
|
+
const selectedRevision = sourceRevision ?? settled?.sourceRevision ?? "";
|
|
53
|
+
const selectedInstallationId = installationId ?? repo;
|
|
54
|
+
const run = useQuery({
|
|
55
|
+
queryKey: ["apps.buildWorkflowRun", runId],
|
|
56
|
+
enabled: runId !== null,
|
|
38
57
|
queryFn: () => {
|
|
39
|
-
if (
|
|
40
|
-
return callMethod("apps.
|
|
58
|
+
if (runId === null) throw new Error("No Workflow run selected");
|
|
59
|
+
return callMethod("apps.buildWorkflowRun", { runId }, runStatusSchema);
|
|
41
60
|
},
|
|
42
|
-
enabled: jobId !== null,
|
|
43
61
|
refetchInterval: (query) =>
|
|
44
|
-
query.state.data
|
|
45
|
-
|
|
46
|
-
|
|
62
|
+
query.state.data !== undefined &&
|
|
63
|
+
(query.state.data.outcome !== "active" ||
|
|
64
|
+
terminal(query.state.data.status))
|
|
65
|
+
? false
|
|
66
|
+
: 2000,
|
|
47
67
|
});
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
const build = useMutation({
|
|
69
|
+
mutationFn: async () => {
|
|
70
|
+
const started = await callMethod(
|
|
71
|
+
"apps.startBuildWorkflow",
|
|
72
|
+
{
|
|
73
|
+
repo,
|
|
74
|
+
sourceRevision: selectedRevision,
|
|
75
|
+
installationId: selectedInstallationId,
|
|
76
|
+
requestId: replayKey(),
|
|
77
|
+
},
|
|
78
|
+
runReply,
|
|
59
79
|
);
|
|
80
|
+
if (
|
|
81
|
+
!("kind" in started) ||
|
|
82
|
+
(started.kind !== "started" && started.kind !== "replay")
|
|
83
|
+
)
|
|
84
|
+
throw new Error("Build Workflow refused: " + JSON.stringify(started));
|
|
85
|
+
return started;
|
|
86
|
+
},
|
|
87
|
+
onSuccess: (started) => {
|
|
88
|
+
setRunId(started.runId);
|
|
60
89
|
},
|
|
61
90
|
});
|
|
62
|
-
|
|
63
|
-
|
|
91
|
+
const registration = useMutation({
|
|
92
|
+
mutationFn: () => callMethod("apps.register", { repo }, appRegistration),
|
|
93
|
+
onSuccess: (registered) => {
|
|
94
|
+
setAppId(registered.appId);
|
|
95
|
+
setSourceRevision(registered.sourceRevision);
|
|
96
|
+
setInstallationId(repo);
|
|
97
|
+
setRunId(null);
|
|
98
|
+
queryClient.setQueryData(statusKey, null);
|
|
99
|
+
build.reset();
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
const busy = build.isPending || registration.isPending;
|
|
64
103
|
return (
|
|
65
104
|
<Card className="w-full max-w-2xl" variant="outline">
|
|
66
105
|
<CardHeader>
|
|
@@ -68,27 +107,45 @@ export function AppBuildPanel() {
|
|
|
68
107
|
</CardHeader>
|
|
69
108
|
<CardContent className="flex flex-col gap-4">
|
|
70
109
|
<p className="text-sm text-[var(--stipe-fg-muted)]">
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
110
|
+
Register the committed App recipe, then start the Workflow to build,
|
|
111
|
+
verify and install your chosen revision. Its steps report what
|
|
112
|
+
actually happened. Only the owner may do this.
|
|
74
113
|
</p>
|
|
114
|
+
<div>
|
|
115
|
+
<Button
|
|
116
|
+
type="button"
|
|
117
|
+
disabled={busy}
|
|
118
|
+
onClick={() => {
|
|
119
|
+
registration.mutate();
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
{registration.isPending ? "Registering…" : "Make this an App"}
|
|
123
|
+
</Button>
|
|
124
|
+
</div>
|
|
125
|
+
{registration.isError ? (
|
|
126
|
+
<p role="alert">{registration.error.message}</p>
|
|
127
|
+
) : null}
|
|
75
128
|
<form
|
|
76
129
|
className="flex flex-col gap-4"
|
|
77
130
|
onSubmit={(event) => {
|
|
78
131
|
event.preventDefault();
|
|
79
|
-
if (
|
|
132
|
+
if (
|
|
133
|
+
selectedAppId &&
|
|
134
|
+
selectedRevision &&
|
|
135
|
+
selectedInstallationId &&
|
|
136
|
+
!busy
|
|
137
|
+
)
|
|
138
|
+
build.mutate();
|
|
80
139
|
}}
|
|
81
140
|
>
|
|
82
141
|
<label className="flex flex-col gap-2" htmlFor="app-build-app-id">
|
|
83
142
|
<span>App id</span>
|
|
84
143
|
<Input
|
|
85
144
|
id="app-build-app-id"
|
|
86
|
-
value={
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
placeholder="app_my-fork"
|
|
91
|
-
disabled={build.isPending}
|
|
145
|
+
value={selectedAppId}
|
|
146
|
+
readOnly
|
|
147
|
+
placeholder="app_myfork"
|
|
148
|
+
disabled={busy}
|
|
92
149
|
/>
|
|
93
150
|
</label>
|
|
94
151
|
<label
|
|
@@ -98,73 +155,104 @@ export function AppBuildPanel() {
|
|
|
98
155
|
<span>Source revision</span>
|
|
99
156
|
<Input
|
|
100
157
|
id="app-build-source-revision"
|
|
101
|
-
value={
|
|
158
|
+
value={selectedRevision}
|
|
102
159
|
onChange={(event) => {
|
|
103
160
|
setSourceRevision(event.target.value);
|
|
104
161
|
}}
|
|
105
|
-
placeholder={
|
|
106
|
-
disabled={
|
|
162
|
+
placeholder={"git:" + "a".repeat(40)}
|
|
163
|
+
disabled={busy}
|
|
107
164
|
/>
|
|
108
165
|
</label>
|
|
109
|
-
{build.isError ? (
|
|
110
|
-
<div role="alert" className="text-sm text-error">
|
|
111
|
-
<p>{build.error.message}</p>
|
|
112
|
-
</div>
|
|
113
|
-
) : null}
|
|
114
|
-
<div>
|
|
115
|
-
<Button
|
|
116
|
-
type="submit"
|
|
117
|
-
disabled={!appId || !sourceRevision || build.isPending}
|
|
118
|
-
>
|
|
119
|
-
{build.isPending ? "Starting build…" : "Build"}
|
|
120
|
-
</Button>
|
|
121
|
-
</div>
|
|
122
|
-
</form>
|
|
123
|
-
{jobId !== null ? (
|
|
124
|
-
<p className="text-sm" role="status">
|
|
125
|
-
Job {jobId}: {settled?.status ?? "running…"}
|
|
126
|
-
{/* A succeeded build's detail is its phase timings, not a message. */}
|
|
127
|
-
{settled?.status !== "succeeded" && settled?.detail
|
|
128
|
-
? ` — ${settled.detail}`
|
|
129
|
-
: ""}
|
|
130
|
-
</p>
|
|
131
|
-
) : null}
|
|
132
|
-
<form
|
|
133
|
-
className="flex flex-col gap-4"
|
|
134
|
-
onSubmit={(event) => {
|
|
135
|
-
event.preventDefault();
|
|
136
|
-
if (installationId && release !== null && !install.isPending)
|
|
137
|
-
install.mutate();
|
|
138
|
-
}}
|
|
139
|
-
>
|
|
140
166
|
<label className="flex flex-col gap-2" htmlFor="app-install-id">
|
|
141
167
|
<span>Install at</span>
|
|
142
168
|
<Input
|
|
143
169
|
id="app-install-id"
|
|
144
|
-
value={
|
|
170
|
+
value={selectedInstallationId}
|
|
145
171
|
onChange={(event) => {
|
|
146
172
|
setInstallationId(event.target.value);
|
|
147
173
|
}}
|
|
148
|
-
|
|
149
|
-
disabled={install.isPending}
|
|
174
|
+
disabled={busy}
|
|
150
175
|
/>
|
|
151
176
|
</label>
|
|
152
|
-
{
|
|
153
|
-
<div role="alert" className="text-sm text-error">
|
|
154
|
-
<p>{install.error.message}</p>
|
|
155
|
-
</div>
|
|
156
|
-
) : null}
|
|
177
|
+
{build.isError ? <p role="alert">{build.error.message}</p> : null}
|
|
157
178
|
<div>
|
|
158
179
|
<Button
|
|
159
180
|
type="submit"
|
|
160
181
|
disabled={
|
|
161
|
-
!
|
|
182
|
+
!selectedAppId ||
|
|
183
|
+
!selectedRevision ||
|
|
184
|
+
!selectedInstallationId ||
|
|
185
|
+
busy
|
|
162
186
|
}
|
|
163
187
|
>
|
|
164
|
-
{
|
|
188
|
+
{build.isPending ? "Starting Workflow…" : "Build & install"}
|
|
165
189
|
</Button>
|
|
166
190
|
</div>
|
|
167
191
|
</form>
|
|
192
|
+
{runId !== null ? (
|
|
193
|
+
<section aria-label="Build Workflow" className="flex flex-col gap-3">
|
|
194
|
+
<p role="status">Workflow run {runId}</p>
|
|
195
|
+
{run.isPending ? (
|
|
196
|
+
<p role="status">Loading Workflow steps…</p>
|
|
197
|
+
) : null}
|
|
198
|
+
{run.isError ? <p role="alert">{run.error.message}</p> : null}
|
|
199
|
+
{run.data === undefined ? null : (
|
|
200
|
+
<>
|
|
201
|
+
<p role="status">
|
|
202
|
+
{run.data.title}: {run.data.status}
|
|
203
|
+
</p>
|
|
204
|
+
{run.data.error === null ? null : (
|
|
205
|
+
<OutputError error={run.data.error} />
|
|
206
|
+
)}
|
|
207
|
+
<RunTimeline
|
|
208
|
+
items={timelineItems(
|
|
209
|
+
runSteps(
|
|
210
|
+
run.data.events,
|
|
211
|
+
run.data.outcome,
|
|
212
|
+
run.data.reviews,
|
|
213
|
+
),
|
|
214
|
+
definition,
|
|
215
|
+
appDisplay,
|
|
216
|
+
)}
|
|
217
|
+
/>
|
|
218
|
+
</>
|
|
219
|
+
)}
|
|
220
|
+
</section>
|
|
221
|
+
) : (
|
|
222
|
+
<section
|
|
223
|
+
aria-label="Latest repository build"
|
|
224
|
+
className="flex flex-col gap-2"
|
|
225
|
+
>
|
|
226
|
+
{status.data != null && "kind" in status.data ? (
|
|
227
|
+
<p role="status">
|
|
228
|
+
Build or automatic installation is still running.
|
|
229
|
+
</p>
|
|
230
|
+
) : null}
|
|
231
|
+
{settled === null ? null : (
|
|
232
|
+
<p role="status">
|
|
233
|
+
Job {settled.jobId}: {settled.status}
|
|
234
|
+
{settled.status !== "succeeded" && settled.detail
|
|
235
|
+
? " — " + settled.detail
|
|
236
|
+
: ""}
|
|
237
|
+
</p>
|
|
238
|
+
)}
|
|
239
|
+
{status.isError ? <p role="alert">{status.error.message}</p> : null}
|
|
240
|
+
{settled?.installation?.kind === "needs_consent" ? (
|
|
241
|
+
<p role="status">
|
|
242
|
+
Needs owner consent. Automatic installation left any previous
|
|
243
|
+
Release unchanged.
|
|
244
|
+
</p>
|
|
245
|
+
) : null}
|
|
246
|
+
{settled?.installation?.kind === "installed" ? (
|
|
247
|
+
<p role="status">
|
|
248
|
+
Installed at {settled.installation.installationId}.
|
|
249
|
+
</p>
|
|
250
|
+
) : null}
|
|
251
|
+
{settled?.installation?.kind === "failed" ? (
|
|
252
|
+
<p role="alert">Not installed: {settled.installation.detail}</p>
|
|
253
|
+
) : null}
|
|
254
|
+
</section>
|
|
255
|
+
)}
|
|
168
256
|
</CardContent>
|
|
169
257
|
</Card>
|
|
170
258
|
);
|
package/web/bonsai.css
CHANGED
package/web/change-page.tsx
CHANGED
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
} from "@fungi.computer/caps/components/data-list";
|
|
15
15
|
import { InlineCode } from "@fungi.computer/caps/components/inline-code";
|
|
16
16
|
import { Status } from "@fungi.computer/caps/components/status";
|
|
17
|
-
import { StatusLed } from "@fungi.computer/caps/components/field-guide";
|
|
18
17
|
import type { MergeResult, PolicyDecision } from "../shared/contract.js";
|
|
19
18
|
import {
|
|
20
19
|
useAgents,
|
|
@@ -569,14 +568,19 @@ export function ChangePage({ repo, number }: { repo: string; number: number }) {
|
|
|
569
568
|
>
|
|
570
569
|
<div className="min-w-0">
|
|
571
570
|
<div className="flex flex-wrap items-center gap-2">
|
|
572
|
-
<
|
|
573
|
-
|
|
571
|
+
<Status
|
|
572
|
+
size="compact"
|
|
574
573
|
tone={
|
|
575
574
|
check.status === "passed"
|
|
576
|
-
? "
|
|
575
|
+
? "success"
|
|
577
576
|
: check.status === "failed"
|
|
578
|
-
? "
|
|
579
|
-
: "
|
|
577
|
+
? "error"
|
|
578
|
+
: check.status === "running"
|
|
579
|
+
? "info"
|
|
580
|
+
: "neutral"
|
|
581
|
+
}
|
|
582
|
+
motion={
|
|
583
|
+
check.status === "running" ? "bounce" : "none"
|
|
580
584
|
}
|
|
581
585
|
label={check.status}
|
|
582
586
|
/>
|
package/web/index.html
CHANGED
package/web/repo-settings.tsx
CHANGED
package/web/public/favicon.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M9 23V9m0 4c0 4 14 2 14 9" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"/><circle cx="9" cy="9" r="3" fill="currentColor"/><circle cx="9" cy="23" r="3" fill="currentColor"/><circle cx="23" cy="22" r="3" fill="currentColor"/></svg>
|