withruntime 0.23.0 → 0.33.1
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/COMPATIBILITY.md +66 -5
- package/E2B.md +1 -1
- package/README.md +36 -2
- package/README.src.md +35 -1
- package/compatibility-lock.json +3 -3
- package/dist/agents/browser.d.ts +12 -0
- package/dist/agents/browser.js +12 -0
- package/dist/agents/index.d.ts +7 -7
- package/dist/agents/index.js +7 -8
- package/dist/agents-cli.d.ts +1 -1
- package/dist/agents-cli.js +250 -3
- package/dist/agents-deploy.d.ts +186 -0
- package/dist/agents-deploy.js +752 -0
- package/dist/agents-file.d.ts +19 -0
- package/dist/agents-file.js +135 -0
- package/dist/apps/index.d.ts +10 -0
- package/dist/apps/index.js +11 -0
- package/dist/apps-cli.d.ts +3 -1
- package/dist/apps-cli.js +285 -38
- package/dist/blaxel/errors.js +2 -1
- package/dist/ci/index.d.ts +13 -0
- package/dist/ci/index.js +14 -0
- package/dist/ci-cli.d.ts +10 -0
- package/dist/ci-cli.js +402 -0
- package/dist/ci-import.d.ts +23 -0
- package/dist/ci-import.js +565 -0
- package/dist/cli.d.ts +18 -7
- package/dist/cli.js +775 -85
- package/dist/daytona/errors.js +2 -1
- package/dist/e2b/errors.js +2 -1
- package/dist/e2b/filesystem.d.ts +1 -0
- package/dist/e2b/filesystem.js +1 -1
- package/dist/e2b/sandbox.d.ts +11 -2
- package/dist/e2b/sandbox.js +22 -6
- package/dist/errors.d.ts +11 -4
- package/dist/errors.js +27 -4
- package/dist/fly/index.d.ts +102 -0
- package/dist/fly/index.js +930 -0
- package/dist/index.d.ts +7 -3
- package/dist/machine.d.ts +10 -6
- package/dist/machine.js +14 -6
- package/dist/modal/index.d.ts +34 -0
- package/dist/modal/index.js +128 -22
- package/dist/products/agents.d.ts +101 -3
- package/dist/products/agents.js +49 -0
- package/dist/products/apps.d.ts +206 -10
- package/dist/products/apps.js +172 -1
- package/dist/products/ci.d.ts +335 -0
- package/dist/products/ci.js +108 -0
- package/dist/products/index.d.ts +8 -0
- package/dist/products/index.js +15 -0
- package/dist/products/jobs.d.ts +105 -5
- package/dist/products/jobs.js +47 -3
- package/dist/products/network.d.ts +13 -0
- package/dist/products/observability.d.ts +1 -1
- package/dist/products/plans.d.ts +131 -0
- package/dist/products/plans.js +24 -0
- package/dist/products/reservations.d.ts +108 -0
- package/dist/products/reservations.js +41 -0
- package/dist/products/resources.d.ts +2 -1
- package/dist/products/secrets.d.ts +90 -6
- package/dist/products/secrets.js +8 -2
- package/dist/proxy.d.ts +3 -3
- package/dist/proxy.js +20 -0
- package/dist/sandbox.d.ts +29 -1
- package/dist/sandbox.js +56 -3
- package/dist/snapshots.d.ts +1 -1
- package/dist/switched-off.d.ts +5 -0
- package/dist/switched-off.js +8 -0
- package/dist/tar.d.ts +1 -1
- package/dist/tar.js +19 -1
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +48 -9
- package/dist/types.d.ts +35 -4
- package/dist/vercel/errors.d.ts +1 -1
- package/dist/vercel/errors.js +2 -1
- package/dist/vercel/snapshot.js +4 -2
- package/package.json +23 -6
package/dist/ci-cli.js
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { importCi } from "./ci-import.js";
|
|
4
|
+
import { me, named } from "./cli-name.js";
|
|
5
|
+
import { ci, } from "./products/ci.js";
|
|
6
|
+
export const CI_HELP = `runtime ci <command>
|
|
7
|
+
|
|
8
|
+
CI/CD checks each push and pull request of a connected GitHub repository in
|
|
9
|
+
sandboxes, as the steps its config file names. A fork's pull request waits
|
|
10
|
+
until a writer re-runs it on GitHub. In a private beta: outside it, each
|
|
11
|
+
command says how to ask to join.
|
|
12
|
+
|
|
13
|
+
import [dir] [--write] [--force] This folder's GitHub Actions workflows and CircleCI
|
|
14
|
+
config as a runtime.yml: printed, or written with
|
|
15
|
+
--write; what isn't carried over is noted at its top.
|
|
16
|
+
Needs no key and sends nothing
|
|
17
|
+
connect <owner/name> [--branch main] [--config runtime.yml] [--installation <id>]
|
|
18
|
+
[--trust <app-slug>[bot]]... [--name <name>]
|
|
19
|
+
Connect a repository; prints its id. Takes a full key.
|
|
20
|
+
--trust runs that App's pushes as its own from the start
|
|
21
|
+
repos [ls] [--all] Connected repositories, with the Apps each trusts;
|
|
22
|
+
--all: disconnected ones too
|
|
23
|
+
repos trust <repoId | owner/name> <app-slug>[bot]...
|
|
24
|
+
Run that App's pushes as the repository's own code,
|
|
25
|
+
keeping the Apps it trusts already
|
|
26
|
+
repos untrust <repoId | owner/name> <app-slug>[bot]...
|
|
27
|
+
Stop trusting those Apps, keeping the rest
|
|
28
|
+
disconnect <repoId | owner/name> Stop checking it; its checks still going are cancelled
|
|
29
|
+
ls [--repository <repoId>] [--branch <name>] [--commit <sha>] [--pr <number>]
|
|
30
|
+
[--state <state>]... [--limit 20] Checks, newest first, with their jobs by state
|
|
31
|
+
get <checkId> One check and its jobs in plan order
|
|
32
|
+
cancel <checkId> Cancel a queued or running check: its jobs end,
|
|
33
|
+
its machines are let go, GitHub is told
|
|
34
|
+
logs <jobId> A job's output, its last 256 KiB, and its sandboxes
|
|
35
|
+
tests <repoId> [--quarantined] Its test files: time, recent outcomes, quarantine
|
|
36
|
+
flakes [--repository <repoId>] [--all] Flaky tests: each quarantined (or still gating), its
|
|
37
|
+
owner and 48-hour deadline; --all: ended ones too
|
|
38
|
+
flake <flakeId> One flaky test: its proof, owner, and recent runs
|
|
39
|
+
flake prove <flakeId> Its 100 proof runs again at main's next commit
|
|
40
|
+
flake hand <flakeId> <login | org/team> Hand it to someone else
|
|
41
|
+
flake delete <flakeId> --reason <why> Say why its test is deleted; it ends once main
|
|
42
|
+
no longer holds the file
|
|
43
|
+
|
|
44
|
+
A failed job's sandbox may be kept paused: \`runtime sandboxes wake <id>\`, then
|
|
45
|
+
\`runtime sandboxes shell <id>\` to look around where it failed.
|
|
46
|
+
|
|
47
|
+
Examples
|
|
48
|
+
runtime ci import --write
|
|
49
|
+
runtime ci connect acme/web --config .runtime/ci.yml
|
|
50
|
+
runtime ci repos trust acme/web acme-bot[bot]
|
|
51
|
+
runtime ci ls --commit $(git rev-parse HEAD)
|
|
52
|
+
runtime ci logs <jobId>
|
|
53
|
+
`;
|
|
54
|
+
const when = (ms) => (ms === null ? "-" : new Date(ms).toISOString());
|
|
55
|
+
const took = (from, to) => from === null || to === null ? "-" : `${((to - from) / 1000).toFixed(1)}s`;
|
|
56
|
+
const counted = (c) => Object.entries(c)
|
|
57
|
+
.map(([state, n]) => `${n} ${state}`)
|
|
58
|
+
.join(", ") || "-";
|
|
59
|
+
/** Where a check's machines came from and how soon, in a line. */
|
|
60
|
+
const machined = (m) => {
|
|
61
|
+
if (!m)
|
|
62
|
+
return null;
|
|
63
|
+
const from = [
|
|
64
|
+
m.reused && `${m.reused} its pull request's own`,
|
|
65
|
+
m.pooled && `${m.pooled} pooled`,
|
|
66
|
+
m.forked && `${m.forked} forked in ${m.forks}`,
|
|
67
|
+
m.created && `${m.created} created`,
|
|
68
|
+
].filter(Boolean);
|
|
69
|
+
const s = (ms) => (ms === null ? "-" : `${(ms / 1000).toFixed(2)}s`);
|
|
70
|
+
return `machines: ${from.join(", ") || "none"}; first in ${s(m.firstMs)}, first test at ${s(m.firstTestMs)}${m.lineMs ? `, ${s(m.lineMs)} in line` : ""}${m.kept ? `; ${m.kept} kept for the next push` : ""}`;
|
|
71
|
+
};
|
|
72
|
+
const where = (c) => c.pullRequest !== null ? `#${c.pullRequest}` : (c.branch ?? c.event);
|
|
73
|
+
const STATES = ["queued", "running", "passed", "failed", "cancelled"];
|
|
74
|
+
const ownerOf = (f) => f.owner === null
|
|
75
|
+
? "nobody found"
|
|
76
|
+
: `@${f.owner.login}${f.owner.pullRequest === null ? "" : ` (#${f.owner.pullRequest})`}`;
|
|
77
|
+
/** `runtime ci import [dir] [--write] [--force]`: the folder's GitHub Actions
|
|
78
|
+
* workflows and CircleCI config as a `runtime.yml`, printed or written. */
|
|
79
|
+
async function importCommand(argv, out, kit) {
|
|
80
|
+
const args = kit.parse(argv, ["write", "force"], []);
|
|
81
|
+
const root = args.positional[0] ?? ".";
|
|
82
|
+
const files = [];
|
|
83
|
+
const workflows = join(root, ".github", "workflows");
|
|
84
|
+
if (existsSync(workflows))
|
|
85
|
+
for (const name of readdirSync(workflows).sort())
|
|
86
|
+
if (/\.ya?ml$/.test(name))
|
|
87
|
+
files.push({
|
|
88
|
+
path: `.github/workflows/${name}`,
|
|
89
|
+
text: readFileSync(join(workflows, name), "utf8"),
|
|
90
|
+
});
|
|
91
|
+
const circle = join(root, ".circleci", "config.yml");
|
|
92
|
+
if (existsSync(circle))
|
|
93
|
+
files.push({ path: ".circleci/config.yml", text: readFileSync(circle, "utf8") });
|
|
94
|
+
if (!files.length)
|
|
95
|
+
throw kit.usage(`${root} has neither .github/workflows/*.yml nor .circleci/config.yml to import.`);
|
|
96
|
+
const made = importCi(files);
|
|
97
|
+
if (!made.yaml) {
|
|
98
|
+
out.error(made.notes.join("\n"));
|
|
99
|
+
throw kit.usage("No job of these runs on a push or a pull request: nothing to write.");
|
|
100
|
+
}
|
|
101
|
+
if (!kit.has(args, "write")) {
|
|
102
|
+
out.write(out.json ? JSON.stringify({ ...made, read: files.map((f) => f.path) }) : made.yaml.trimEnd());
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
const target = join(root, "runtime.yml");
|
|
106
|
+
if (existsSync(target) && !kit.has(args, "force"))
|
|
107
|
+
throw kit.usage(`${target} is there already: --force writes over it.`);
|
|
108
|
+
writeFileSync(target, made.yaml);
|
|
109
|
+
const notes = made.notes.length
|
|
110
|
+
? `, ${made.notes.length} notes on what was not carried over at its top`
|
|
111
|
+
: "";
|
|
112
|
+
out.write(out.json
|
|
113
|
+
? JSON.stringify({ ...made, read: files.map((f) => f.path), written: target })
|
|
114
|
+
: `${target}: ${made.steps.length} steps from ${files.length === 1 ? "1 file" : `${files.length} files`}${notes}. Push it, then \`${me} ci connect <owner/name>\`.`);
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
export async function ciCommand(argv, out, client, kit) {
|
|
118
|
+
const { parse, flag, has, need, table, usage, unknown } = kit;
|
|
119
|
+
const [verb, ...rest] = argv;
|
|
120
|
+
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
121
|
+
if (!verb || verb === "help" || verb === "--help") {
|
|
122
|
+
print(named(CI_HELP), { usage: named(CI_HELP) });
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
// Reads and writes this folder's files only: no key, nothing sent.
|
|
126
|
+
if (verb === "import")
|
|
127
|
+
return importCommand(rest, out, kit);
|
|
128
|
+
const api = ci((await client()).transport);
|
|
129
|
+
const whole = (value, what) => {
|
|
130
|
+
if (value === undefined)
|
|
131
|
+
return undefined;
|
|
132
|
+
if (!/^[1-9][0-9]{0,8}$/.test(value))
|
|
133
|
+
throw usage(`${what} takes a whole number: ${value}`);
|
|
134
|
+
return Number(value);
|
|
135
|
+
};
|
|
136
|
+
const apps = (words) => {
|
|
137
|
+
if (!words.length)
|
|
138
|
+
throw usage(`Name an App by its login, such as acme-bot[bot].`);
|
|
139
|
+
for (const word of words)
|
|
140
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9-]{0,38}\[bot\]$/.test(word))
|
|
141
|
+
throw usage(`${word} is not an App's login: name it <app-slug>[bot], as GitHub shows its pushes (quote it in your shell: '${word.replace(/\[bot\]$/, "")}[bot]').`);
|
|
142
|
+
return words;
|
|
143
|
+
};
|
|
144
|
+
const trusted = (r) => r.trustedApps?.join(", ") || "none";
|
|
145
|
+
const disconnect = async (idOrName) => {
|
|
146
|
+
const found = await api.repositories.find(idOrName);
|
|
147
|
+
const repo = await api.repositories.disconnect(found?.id ?? idOrName);
|
|
148
|
+
print(`${repo.repository} disconnected; its checks that ended stay.`, repo);
|
|
149
|
+
return 0;
|
|
150
|
+
};
|
|
151
|
+
switch (verb) {
|
|
152
|
+
case "connect": {
|
|
153
|
+
const args = parse(rest, [], ["branch", "config", "installation", "trust", "name"]);
|
|
154
|
+
const repository = need(args.positional[0], "the repository, such as acme/web");
|
|
155
|
+
const installation = whole(flag(args, "installation"), "--installation");
|
|
156
|
+
const trust = args.flags.get("trust") ?? [];
|
|
157
|
+
const input = {
|
|
158
|
+
repository,
|
|
159
|
+
...(flag(args, "branch") ? { productionBranch: flag(args, "branch") } : {}),
|
|
160
|
+
...(flag(args, "config") ? { configPath: flag(args, "config") } : {}),
|
|
161
|
+
...(installation === undefined ? {} : { installation }),
|
|
162
|
+
...(trust.length ? { trustedApps: apps(trust) } : {}),
|
|
163
|
+
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
164
|
+
};
|
|
165
|
+
const repo = await api.repositories.connect(input);
|
|
166
|
+
if (!out.json)
|
|
167
|
+
out.error(`Connected ${repo.repository}: each push and pull request is checked as ${repo.configPath} says; ${repo.productionBranch} is its production branch; it trusts ${trusted(repo)}. Its checks: ${me} ci ls --repository ${repo.id}`);
|
|
168
|
+
print(repo.id, repo);
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
case "repos":
|
|
172
|
+
case "repositories": {
|
|
173
|
+
const [action, ...more] = rest;
|
|
174
|
+
if (action === "trust" || action === "untrust") {
|
|
175
|
+
const [which, ...logins] = more;
|
|
176
|
+
const id = need(which, "the repository's id or owner/name");
|
|
177
|
+
const listed = apps(logins);
|
|
178
|
+
const repo = action === "trust"
|
|
179
|
+
? await api.repositories.trust(id, listed)
|
|
180
|
+
: await api.repositories.untrust(id, listed);
|
|
181
|
+
print(`${repo.repository} trusts ${trusted(repo)}.`, repo);
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
if (action === "connect")
|
|
185
|
+
return ciCommand(["connect", ...more], out, client, kit);
|
|
186
|
+
if (action === "disconnect" || action === "rm")
|
|
187
|
+
return disconnect(need(more[0], "the repository's id or owner/name"));
|
|
188
|
+
const listing = action === "ls" || action === "list" ? more : rest;
|
|
189
|
+
if (listing.length && !listing[0].startsWith("-"))
|
|
190
|
+
throw unknown("ci repos command", listing[0], [
|
|
191
|
+
"ls",
|
|
192
|
+
"connect",
|
|
193
|
+
"trust",
|
|
194
|
+
"untrust",
|
|
195
|
+
"disconnect",
|
|
196
|
+
]);
|
|
197
|
+
const args = parse(listing, ["all"], []);
|
|
198
|
+
const all = await (await api.repositories.list({
|
|
199
|
+
limit: 100,
|
|
200
|
+
...(has(args, "all") ? {} : { state: ["connected"] }),
|
|
201
|
+
})).toArray(Infinity);
|
|
202
|
+
print(all.length
|
|
203
|
+
? table([
|
|
204
|
+
["ID", "REPOSITORY", "BRANCH", "CONFIG", "TRUSTED APPS", "STATE"],
|
|
205
|
+
...all.map((r) => [
|
|
206
|
+
r.id,
|
|
207
|
+
r.repository,
|
|
208
|
+
r.productionBranch,
|
|
209
|
+
r.configPath,
|
|
210
|
+
trusted(r),
|
|
211
|
+
r.state,
|
|
212
|
+
]),
|
|
213
|
+
])
|
|
214
|
+
: `No repositories this key lists. Connect one: ${me} ci connect acme/web`, all);
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
case "disconnect":
|
|
218
|
+
return disconnect(need(rest[0], "the repository's id or owner/name"));
|
|
219
|
+
case "ls":
|
|
220
|
+
case "list": {
|
|
221
|
+
const args = parse(rest, [], ["repository", "branch", "commit", "pr", "state", "limit"]);
|
|
222
|
+
const commit = flag(args, "commit");
|
|
223
|
+
if (commit !== undefined && !/^([0-9a-f]{40}|[0-9a-f]{64})$/.test(commit))
|
|
224
|
+
throw usage(`--commit takes a full sha in lower case: ${me} ci ls --commit $(git rev-parse HEAD)`);
|
|
225
|
+
const states = args.flags.get("state");
|
|
226
|
+
for (const state of states ?? [])
|
|
227
|
+
if (!STATES.includes(state))
|
|
228
|
+
throw unknown("state", state, STATES);
|
|
229
|
+
const pullRequest = whole(flag(args, "pr"), "--pr");
|
|
230
|
+
const limit = whole(flag(args, "limit"), "--limit") ?? 20;
|
|
231
|
+
const page = await api.checks.list({
|
|
232
|
+
limit: Math.min(limit, 100),
|
|
233
|
+
...(flag(args, "repository") ? { repositoryId: flag(args, "repository") } : {}),
|
|
234
|
+
...(flag(args, "branch") ? { branch: flag(args, "branch") } : {}),
|
|
235
|
+
...(commit ? { commit } : {}),
|
|
236
|
+
...(pullRequest === undefined ? {} : { pullRequest }),
|
|
237
|
+
...(states ? { state: states } : {}),
|
|
238
|
+
});
|
|
239
|
+
const all = await page.toArray(limit);
|
|
240
|
+
print(all.length
|
|
241
|
+
? table([
|
|
242
|
+
["ID", "STATE", "REPOSITORY", "WHERE", "COMMIT", "JOBS", "TOOK", "CREATED"],
|
|
243
|
+
...all.map((c) => [
|
|
244
|
+
c.id,
|
|
245
|
+
c.waiting ? "waiting" : c.state,
|
|
246
|
+
c.repository,
|
|
247
|
+
where(c),
|
|
248
|
+
c.commit.slice(0, 12),
|
|
249
|
+
counted(c.counts),
|
|
250
|
+
took(c.startedAt, c.endedAt),
|
|
251
|
+
when(c.createdAt),
|
|
252
|
+
]),
|
|
253
|
+
])
|
|
254
|
+
: "No checks.", all);
|
|
255
|
+
return 0;
|
|
256
|
+
}
|
|
257
|
+
case "get": {
|
|
258
|
+
const check = await api.checks.get(need(rest[0], "a check's id"));
|
|
259
|
+
const job = (j) => [
|
|
260
|
+
j.id,
|
|
261
|
+
j.parts > 1 ? `${j.name} (${j.part}/${j.parts})` : j.name,
|
|
262
|
+
j.state,
|
|
263
|
+
j.ms === null ? "-" : `${(j.ms / 1000).toFixed(1)}s`,
|
|
264
|
+
j.exitCode === null ? "-" : String(j.exitCode),
|
|
265
|
+
j.sandboxIds.join(",") || "-",
|
|
266
|
+
];
|
|
267
|
+
print([
|
|
268
|
+
`${check.repository} ${check.commit.slice(0, 12)} ${where(check)}: ${check.waiting ? "waiting for a writer to re-run it on GitHub" : check.state}${check.reason ? ` (${check.reason})` : ""}`,
|
|
269
|
+
...(check.machines ? [machined(check.machines)] : []),
|
|
270
|
+
check.jobs.length
|
|
271
|
+
? table([["JOB", "NAME", "STATE", "TOOK", "EXIT", "SANDBOXES"], ...check.jobs.map(job)])
|
|
272
|
+
: "No jobs yet.",
|
|
273
|
+
].join("\n"), check);
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
276
|
+
case "cancel": {
|
|
277
|
+
const check = await api.checks.cancel(need(rest[0], "a check's id (runtime ci ls)"));
|
|
278
|
+
print(`${check.repository} ${check.commit.slice(0, 12)} ${where(check)}: ${check.state}${check.reason ? ` (${check.reason})` : ""}`, check);
|
|
279
|
+
return 0;
|
|
280
|
+
}
|
|
281
|
+
case "logs":
|
|
282
|
+
case "job": {
|
|
283
|
+
const job = await api.jobs.get(need(rest[0], "a job's id (runtime ci get <checkId>)"));
|
|
284
|
+
if (out.json) {
|
|
285
|
+
out.write(JSON.stringify(job));
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
out.write(job.log.endsWith("\n") || !job.log ? job.log : `${job.log}\n`);
|
|
289
|
+
if (job.review)
|
|
290
|
+
out.write(reviewed(job.review));
|
|
291
|
+
out.error(`[${job.name}${job.parts > 1 ? ` ${job.part}/${job.parts}` : ""} ${job.state}${job.exitCode === null ? "" : `, exit code ${job.exitCode}`}${job.reason ? `, ${job.reason}` : ""}]`);
|
|
292
|
+
if (job.state === "failed" && job.sandboxIds.length)
|
|
293
|
+
out.error(`Its sandbox may be kept paused, to look into: ${me} sandboxes wake ${job.sandboxIds[0]}, then ${me} sandboxes shell ${job.sandboxIds[0]}`);
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
case "flakes": {
|
|
297
|
+
const args = parse(rest, ["all"], ["repository"]);
|
|
298
|
+
const repositoryId = flag(args, "repository");
|
|
299
|
+
const all = await (await api.flakes.list({
|
|
300
|
+
limit: 100,
|
|
301
|
+
...(repositoryId ? { repositoryId } : {}),
|
|
302
|
+
...(has(args, "all") ? {} : { state: ["quarantined", "gating"] }),
|
|
303
|
+
})).toArray(Infinity);
|
|
304
|
+
print(all.length
|
|
305
|
+
? table([
|
|
306
|
+
["ID", "FILE", "STATE", "OWNER", "DUE"],
|
|
307
|
+
...all.map((f) => [
|
|
308
|
+
f.id,
|
|
309
|
+
f.file,
|
|
310
|
+
f.state,
|
|
311
|
+
ownerOf(f),
|
|
312
|
+
f.endedAt !== null ? "-" : `${when(f.dueAt)}${f.overdue ? " (overdue)" : ""}`,
|
|
313
|
+
]),
|
|
314
|
+
])
|
|
315
|
+
: "No flaky tests.", all);
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
318
|
+
case "flake": {
|
|
319
|
+
const [sub, ...more] = rest;
|
|
320
|
+
if (sub === "prove" || sub === "hand" || sub === "delete") {
|
|
321
|
+
const args = parse(more, [], ["reason"]);
|
|
322
|
+
const id = need(args.positional[0], "a flake's id (runtime ci flakes)");
|
|
323
|
+
const change = sub === "prove"
|
|
324
|
+
? { prove: true }
|
|
325
|
+
: sub === "hand"
|
|
326
|
+
? { owner: need(args.positional[1], "a GitHub login, or org/team") }
|
|
327
|
+
: { deleteReason: need(flag(args, "reason"), "--reason <why its test is deleted>") };
|
|
328
|
+
const f = await api.flakes.update(id, change);
|
|
329
|
+
print(sub === "prove"
|
|
330
|
+
? `${f.file}: its 100 proof runs come at main's next commit.\n`
|
|
331
|
+
: sub === "hand"
|
|
332
|
+
? `${f.file}: handed to ${ownerOf(f)}.\n`
|
|
333
|
+
: `${f.file}: it ends once main no longer holds the file.\n`, f);
|
|
334
|
+
return 0;
|
|
335
|
+
}
|
|
336
|
+
const f = await api.flakes.get(need(sub, "a flake's id (runtime ci flakes)"));
|
|
337
|
+
const runs = f.runs
|
|
338
|
+
.slice(0, 20)
|
|
339
|
+
.map((r) => `${r.passed ? "pass" : "FAIL"} ${r.attempt}`)
|
|
340
|
+
.join(", ");
|
|
341
|
+
print([
|
|
342
|
+
`${f.file}: ${f.state}${f.gates ? ` (still gates: ${f.gates.words})` : ""}`,
|
|
343
|
+
` owner ${ownerOf(f)}, due ${when(f.dueAt)}${f.overdue ? " (overdue)" : ""}`,
|
|
344
|
+
` proof passed and failed under key ${f.proof.key.slice(0, 12)} at ${f.proof.commit.slice(0, 9)}`,
|
|
345
|
+
` runs ${runs || "-"}`,
|
|
346
|
+
...f.failures
|
|
347
|
+
.slice(0, 5)
|
|
348
|
+
.map((x) => ` failed ${x.jobId} at ${x.commit.slice(0, 9)}: ${x.message ?? ""} (${me} ci logs ${x.jobId})`),
|
|
349
|
+
...(f.deleteReason ? [` delete ${f.deleteReason}`] : []),
|
|
350
|
+
"",
|
|
351
|
+
].join("\n"), f);
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
case "tests": {
|
|
355
|
+
const args = parse(rest, ["quarantined"], []);
|
|
356
|
+
const repositoryId = need(args.positional[0], "a repository's id (runtime ci repos)");
|
|
357
|
+
const all = await (await api.tests.list(repositoryId, {
|
|
358
|
+
limit: 100,
|
|
359
|
+
...(has(args, "quarantined") ? { quarantined: true } : {}),
|
|
360
|
+
})).toArray(Infinity);
|
|
361
|
+
print(all.length
|
|
362
|
+
? table([
|
|
363
|
+
["FILE", "TOOK", "RECENT", "QUARANTINED"],
|
|
364
|
+
...all.map((t) => [
|
|
365
|
+
t.file,
|
|
366
|
+
`${(t.ms / 1000).toFixed(1)}s`,
|
|
367
|
+
t.runs
|
|
368
|
+
.slice(-5)
|
|
369
|
+
.map((r) => r.outcome)
|
|
370
|
+
.join(" "),
|
|
371
|
+
t.quarantinedSince === null ? "-" : when(t.quarantinedSince),
|
|
372
|
+
]),
|
|
373
|
+
])
|
|
374
|
+
: "No test files yet.", all);
|
|
375
|
+
return 0;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
throw unknown("ci command", verb, [
|
|
379
|
+
"connect",
|
|
380
|
+
"repos",
|
|
381
|
+
"disconnect",
|
|
382
|
+
"ls",
|
|
383
|
+
"get",
|
|
384
|
+
"cancel",
|
|
385
|
+
"logs",
|
|
386
|
+
"tests",
|
|
387
|
+
"flakes",
|
|
388
|
+
"flake",
|
|
389
|
+
]);
|
|
390
|
+
}
|
|
391
|
+
/** A review's verdict in words: each finding with its place and fix, then
|
|
392
|
+
* the notes, then who reviewed. */
|
|
393
|
+
function reviewed(r) {
|
|
394
|
+
const at = (f) => (f.line > 0 ? `${f.file}:${f.line}` : f.file);
|
|
395
|
+
return [
|
|
396
|
+
`\n${r.verdict === "pass" ? "Passed review" : `Blocked: ${r.findings.length} finding(s)`}. ${r.summary}`,
|
|
397
|
+
...r.findings.map((f) => ` ${f.kind} at ${at(f)}: ${f.what}\n Fix: ${f.fix}`),
|
|
398
|
+
...r.notes.map((n) => ` note at ${at(n)}: ${n.what}`),
|
|
399
|
+
`Reviewed by agent ${r.agent} (${r.model}) on ${r.account}${r.cut ? ", diff cut short" : ""}: session ${r.session}`,
|
|
400
|
+
"",
|
|
401
|
+
].join("\n");
|
|
402
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type CiSource = {
|
|
2
|
+
path: string;
|
|
3
|
+
text: string;
|
|
4
|
+
};
|
|
5
|
+
export type CiImport = {
|
|
6
|
+
/** The `runtime.yml`, its notes on top as comments; `null` when no job runs on a push or pull request. */
|
|
7
|
+
yaml: string | null;
|
|
8
|
+
/** What was not carried over, or carried differently, one line each. */
|
|
9
|
+
notes: string[];
|
|
10
|
+
/** The steps written, by name. */
|
|
11
|
+
steps: string[];
|
|
12
|
+
};
|
|
13
|
+
/** Every combination of a GitHub `strategy.matrix`, its `exclude` and `include` applied. */
|
|
14
|
+
export declare function matrixCells(matrix: Record<string, unknown> | undefined): Record<string, string>[];
|
|
15
|
+
/** Files a cache key hashes: GitHub's `hashFiles('a', 'b')` or CircleCI's `{{ checksum "a" }}`. */
|
|
16
|
+
export declare function cacheFiles(key: string): string[];
|
|
17
|
+
/** Whether a line installs the project's own dependencies and nothing else:
|
|
18
|
+
* `npm ci` or `pip install -r requirements.txt`, never `npm install pino@9`,
|
|
19
|
+
* which one matrix cell asks and another doesn't. */
|
|
20
|
+
export declare function installsOnly(line: string): boolean;
|
|
21
|
+
/** `runtime.yml` from GitHub Actions workflows (`.github/workflows/*.yml`)
|
|
22
|
+
* and a CircleCI config (`.circleci/config.yml`), each given with its path. */
|
|
23
|
+
export declare function importCi(files: CiSource[]): CiImport;
|