laneyard 0.1.0 → 0.3.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/README.md +238 -32
- package/dist/src/cli/detect.js +60 -16
- package/dist/src/cli/prompt.js +36 -0
- package/dist/src/cli/secret.js +133 -0
- package/dist/src/cli/setup.js +282 -0
- package/dist/src/cli/style.js +31 -0
- package/dist/src/cli/user.js +127 -0
- package/dist/src/config/accounts.js +163 -0
- package/dist/src/config/load.js +61 -1
- package/dist/src/config/schema.js +47 -4
- package/dist/src/config/store.js +10 -0
- package/dist/src/config/yaml.js +14 -0
- package/dist/src/db/cache.js +8 -8
- package/dist/src/db/open.js +15 -0
- package/dist/src/db/runs.js +51 -6
- package/dist/src/db/schema.sql +20 -2
- package/dist/src/db/secrets.js +64 -0
- package/dist/src/fastfile/store.js +53 -0
- package/dist/src/git/workspace.js +95 -5
- package/dist/src/heuristics/blocking-actions.js +65 -0
- package/dist/src/heuristics/platforms.js +77 -0
- package/dist/src/heuristics/readiness.js +336 -0
- package/dist/src/logs/redact.js +86 -0
- package/dist/src/main.js +79 -9
- package/dist/src/runner/orchestrate.js +60 -10
- package/dist/src/runner/pty.js +27 -15
- package/dist/src/runner/queue.js +114 -0
- package/dist/src/secrets/cipher.js +28 -0
- package/dist/src/secrets/key.js +40 -0
- package/dist/src/secrets/vault.js +68 -0
- package/dist/src/server/app.js +135 -13
- package/dist/src/server/auth.js +85 -17
- package/dist/src/server/permissions.js +73 -0
- package/dist/src/server/routes/fastfile.js +131 -0
- package/dist/src/server/routes/projects.js +50 -1
- package/dist/src/server/routes/readiness.js +102 -0
- package/dist/src/server/routes/runs.js +27 -45
- package/dist/src/server/routes/secrets.js +51 -0
- package/dist/src/server/routes/users.js +64 -0
- package/dist/src/sidecar/bridge.js +17 -1
- package/dist/src/sidecar/lanes.js +2 -2
- package/dist/src/sidecar/uses.js +40 -0
- package/dist/web/assets/Editor-DNFBA4gv.js +14 -0
- package/dist/web/assets/index-De6sxx6G.css +1 -0
- package/dist/web/assets/index-TWRQ1cJg.js +62 -0
- package/dist/web/index.html +2 -2
- package/package.json +13 -5
- package/ruby/introspect.rb +80 -0
- package/dist/tests/cli/add.test.js +0 -64
- package/dist/tests/cli/detect.test.js +0 -63
- package/dist/tests/config/load.test.js +0 -68
- package/dist/tests/config/resolve.test.js +0 -44
- package/dist/tests/config/store.test.js +0 -58
- package/dist/tests/db/runs.test.js +0 -54
- package/dist/tests/e2e/full-thread.test.js +0 -65
- package/dist/tests/fixtures/repos.js +0 -30
- package/dist/tests/git/workspace.test.js +0 -66
- package/dist/tests/heuristics/error-summary.test.js +0 -31
- package/dist/tests/logs/store.test.js +0 -38
- package/dist/tests/main.test.js +0 -27
- package/dist/tests/ruby/introspect.test.js +0 -59
- package/dist/tests/runner/artifacts.test.js +0 -49
- package/dist/tests/runner/live-steps.test.js +0 -35
- package/dist/tests/runner/orchestrate.test.js +0 -124
- package/dist/tests/runner/pty.test.js +0 -53
- package/dist/tests/runner/report.test.js +0 -91
- package/dist/tests/server/api.test.js +0 -132
- package/dist/tests/server/auth.test.js +0 -53
- package/dist/tests/server/ws.test.js +0 -43
- package/dist/tests/sidecar/lanes.test.js +0 -52
- package/dist/tests/sidecar/ruby-env.test.js +0 -25
- package/dist/tests/smoke.test.js +0 -7
- package/dist/web/assets/index-583x0xuo.css +0 -1
- package/dist/web/assets/index-BEpABKPS.js +0 -61
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { MIN_PASSWORD_LENGTH, VALID_NAME, refusalFor, removeUserFromConfig, upsertUserInConfig, } from "../../config/accounts.js";
|
|
2
|
+
const ROLES = ["admin", "builder"];
|
|
3
|
+
/**
|
|
4
|
+
* The accounts, as the interface sees them.
|
|
5
|
+
*
|
|
6
|
+
* Every route here is on the admin list in `permissions.ts`, so nothing below
|
|
7
|
+
* checks a role: the hook already refused anyone who has no business here, and
|
|
8
|
+
* a second check inside a handler is a permission nobody finds during an audit.
|
|
9
|
+
*/
|
|
10
|
+
export async function registerUserRoutes(app, ctx) {
|
|
11
|
+
const accounts = () => ctx.config.server()?.users ?? [];
|
|
12
|
+
// Name and role, and that is the whole shape. The hash is not truncated or
|
|
13
|
+
// masked on the way out — it is simply never put on the wire.
|
|
14
|
+
app.get("/api/users", async () => accounts().map((u) => ({ name: u.name, role: u.role })));
|
|
15
|
+
app.post("/api/users", async (req, reply) => {
|
|
16
|
+
const { name, role, password } = (req.body ?? {});
|
|
17
|
+
if (typeof name !== "string" || !VALID_NAME.test(name)) {
|
|
18
|
+
return reply.code(400).send({
|
|
19
|
+
error: "A name is letters, digits, dot, dash and underscore, starting with a letter or a digit.",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (typeof role !== "string" || !ROLES.includes(role)) {
|
|
23
|
+
return reply.code(400).send({ error: `A role is ${ROLES.join(" or ")}.` });
|
|
24
|
+
}
|
|
25
|
+
if (typeof password !== "string" || password.length < MIN_PASSWORD_LENGTH) {
|
|
26
|
+
return reply
|
|
27
|
+
.code(400)
|
|
28
|
+
.send({ error: `A password is at least ${MIN_PASSWORD_LENGTH} characters.` });
|
|
29
|
+
}
|
|
30
|
+
const refusal = refusalFor(accounts(), name, role);
|
|
31
|
+
if (refusal)
|
|
32
|
+
return reply.code(409).send({ error: refusal });
|
|
33
|
+
const { created } = await upsertUserInConfig(ctx.config.configPath(), {
|
|
34
|
+
name,
|
|
35
|
+
role: role,
|
|
36
|
+
password,
|
|
37
|
+
});
|
|
38
|
+
// Replacing an account changes the password and possibly the role, and a
|
|
39
|
+
// session holds neither — it holds a snapshot taken at login. The sessions
|
|
40
|
+
// go, so that what was just written is what is true everywhere.
|
|
41
|
+
if (!created)
|
|
42
|
+
ctx.sessions.revokeAllFor(name);
|
|
43
|
+
// The file is watched, but on a debounce: reloading here is what makes the
|
|
44
|
+
// very next request — the listing this page is about to ask for — truthful,
|
|
45
|
+
// and what lets the new account log in immediately.
|
|
46
|
+
await ctx.config.load();
|
|
47
|
+
return reply.code(created ? 201 : 200).send({ name, role });
|
|
48
|
+
});
|
|
49
|
+
app.delete("/api/users/:name", async (req, reply) => {
|
|
50
|
+
const { name } = req.params;
|
|
51
|
+
if (!accounts().some((u) => u.name === name)) {
|
|
52
|
+
return reply.code(404).send({ error: "Unknown account" });
|
|
53
|
+
}
|
|
54
|
+
const refusal = refusalFor(accounts(), name, null);
|
|
55
|
+
if (refusal)
|
|
56
|
+
return reply.code(409).send({ error: refusal });
|
|
57
|
+
const removed = await removeUserFromConfig(ctx.config.configPath(), name);
|
|
58
|
+
if (!removed)
|
|
59
|
+
return reply.code(404).send({ error: "Unknown account" });
|
|
60
|
+
ctx.sessions.revokeAllFor(name);
|
|
61
|
+
await ctx.config.load();
|
|
62
|
+
return reply.code(204).send();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { promisify } from "node:util";
|
|
5
6
|
import { FASTLANE_UNAVAILABLE, resolveRubyEnv } from "./ruby-env.js";
|
|
6
7
|
const exec = promisify(execFile);
|
|
7
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Locates `ruby/introspect.rb` from wherever this module happens to live.
|
|
10
|
+
*
|
|
11
|
+
* Two layouts, and only trying one of them is how listing lanes came to be
|
|
12
|
+
* broken in every installed copy while working perfectly from the sources:
|
|
13
|
+
* `src/sidecar/` sits two levels under the package root, `dist/src/sidecar/`
|
|
14
|
+
* sits three. The package ships `ruby/` at its root in both cases.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveSidecarScript(moduleDir) {
|
|
17
|
+
const candidates = [
|
|
18
|
+
join(moduleDir, "..", "..", "ruby", "introspect.rb"),
|
|
19
|
+
join(moduleDir, "..", "..", "..", "ruby", "introspect.rb"),
|
|
20
|
+
];
|
|
21
|
+
return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0];
|
|
22
|
+
}
|
|
23
|
+
const SCRIPT = resolveSidecarScript(dirname(fileURLToPath(import.meta.url)));
|
|
8
24
|
/**
|
|
9
25
|
* Runs the sidecar in the project's context.
|
|
10
26
|
* In `bundle` mode, the invocation goes through `bundle exec` to see the
|
|
@@ -27,14 +27,14 @@ export class LaneReader {
|
|
|
27
27
|
}
|
|
28
28
|
async read(slug, workspacePath, fastlaneDir) {
|
|
29
29
|
const hash = await hashFastlaneDir(workspacePath, fastlaneDir);
|
|
30
|
-
const cached = this.cache.get(slug, hash);
|
|
30
|
+
const cached = this.cache.get(slug, "lanes", hash);
|
|
31
31
|
if (cached)
|
|
32
32
|
return cached;
|
|
33
33
|
const res = await this.invoke("lanes", workspacePath, fastlaneDir);
|
|
34
34
|
if (!res.ok)
|
|
35
35
|
throw new Error(res.error);
|
|
36
36
|
const lanes = res["lanes"];
|
|
37
|
-
this.cache.put(slug, hash, lanes);
|
|
37
|
+
this.cache.put(slug, "lanes", hash, lanes);
|
|
38
38
|
return lanes;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Hash of the whole fastlane folder, not just the Fastfile:
|
|
6
|
+
* an Appfile, a Pluginfile, or an imported file change the lanes just as much.
|
|
7
|
+
*/
|
|
8
|
+
async function hashFastlaneDir(root, fastlaneDir) {
|
|
9
|
+
const dir = join(root, fastlaneDir);
|
|
10
|
+
const hash = createHash("sha256");
|
|
11
|
+
const entries = (await readdir(dir, { withFileTypes: true, recursive: true }))
|
|
12
|
+
.filter((e) => e.isFile())
|
|
13
|
+
.map((e) => join(e.parentPath, e.name))
|
|
14
|
+
.sort();
|
|
15
|
+
for (const file of entries) {
|
|
16
|
+
hash.update(file);
|
|
17
|
+
hash.update(await readFile(file));
|
|
18
|
+
}
|
|
19
|
+
return hash.digest("hex");
|
|
20
|
+
}
|
|
21
|
+
export class UsesReader {
|
|
22
|
+
cache;
|
|
23
|
+
invoke;
|
|
24
|
+
constructor(cache, invoke) {
|
|
25
|
+
this.cache = cache;
|
|
26
|
+
this.invoke = invoke;
|
|
27
|
+
}
|
|
28
|
+
async read(slug, workspacePath, fastlaneDir) {
|
|
29
|
+
const hash = await hashFastlaneDir(workspacePath, fastlaneDir);
|
|
30
|
+
const cached = this.cache.get(slug, "uses", hash);
|
|
31
|
+
if (cached)
|
|
32
|
+
return cached;
|
|
33
|
+
const res = await this.invoke("uses", workspacePath, fastlaneDir);
|
|
34
|
+
if (!res.ok)
|
|
35
|
+
throw new Error(res.error);
|
|
36
|
+
const lanes = res["lanes"];
|
|
37
|
+
this.cache.put(slug, "uses", hash, lanes);
|
|
38
|
+
return lanes;
|
|
39
|
+
}
|
|
40
|
+
}
|