@feigi/fleet-ctl 3.19.4 → 3.19.6
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/package.json
CHANGED
package/scripts/recipe-prove.mjs
CHANGED
|
@@ -45,8 +45,9 @@
|
|
|
45
45
|
// Exit 1: NOT PROVEN — no cache written; the reason on stderr.
|
|
46
46
|
// Exit 2: no verdict on the proof — it could not be attempted (usage, not a
|
|
47
47
|
// repository, origin/main missing, the worktree could not be made), or
|
|
48
|
-
// a filesystem fault stopped it before the cache was settled
|
|
49
|
-
// cache or its temp file
|
|
48
|
+
// a git or filesystem fault stopped it before the cache was settled
|
|
49
|
+
// (a git command could not be started; the cache or its temp file
|
|
50
|
+
// could not be written).
|
|
50
51
|
|
|
51
52
|
import { spawnSync } from "node:child_process";
|
|
52
53
|
import { closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -108,8 +109,16 @@ function parseArgs(argv) {
|
|
|
108
109
|
};
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
// A git that cannot be started is no verdict, as for `sh` below: read as a
|
|
113
|
+
// failed git command, it would surface as whatever that command's failure
|
|
114
|
+
// means to its caller — a missing repository, an unfetched origin/main, a
|
|
115
|
+
// proof that did not hold. Only a spawn that produced no process counts:
|
|
116
|
+
// spawnSync also sets `error` for a git that ran and was killed — ENOBUFS,
|
|
117
|
+
// when its output outgrows the default maxBuffer — and that is a failed git
|
|
118
|
+
// command like any other, never a git that could not be started.
|
|
111
119
|
function git(args, cwd) {
|
|
112
120
|
const r = spawnSync("git", args, { cwd, env: ENV, encoding: "utf8" });
|
|
121
|
+
if (r.error && !r.pid) throw cannot(`could not start git: ${r.error.message}`);
|
|
113
122
|
return { ok: r.status === 0, out: (r.stdout ?? "").replace(/\n$/, ""), err: (r.stderr ?? "").trim() };
|
|
114
123
|
}
|
|
115
124
|
|
|
@@ -231,9 +240,19 @@ function writeCache(cache, recipe, repo) {
|
|
|
231
240
|
// is the usual one; the repo ignores them) makes both git's removal and a
|
|
232
241
|
// plain recursive delete fail, so the fallback makes the tree writable first.
|
|
233
242
|
// The prune always runs: it is what drops the registration of a worktree
|
|
234
|
-
// whose directory is gone.
|
|
243
|
+
// whose directory is gone. A git that cannot be started is reported and
|
|
244
|
+
// skipped here, since this runs in the proof's `finally`.
|
|
235
245
|
function removeWorktree(wt, repo) {
|
|
236
|
-
|
|
246
|
+
const tryGit = (args) => {
|
|
247
|
+
try {
|
|
248
|
+
return git(args, repo).ok;
|
|
249
|
+
} catch (e) {
|
|
250
|
+
if (!(e instanceof Refusal)) throw e;
|
|
251
|
+
process.stderr.write(`${NAME}: skipped git ${args.join(" ")}: ${e.message}\n`);
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
if (!tryGit(["worktree", "remove", "--force", wt])) {
|
|
237
256
|
spawnSync("chmod", ["-R", "u+w", wt], { stdio: "ignore" });
|
|
238
257
|
try {
|
|
239
258
|
rmSync(wt, { recursive: true, force: true });
|
|
@@ -241,7 +260,7 @@ function removeWorktree(wt, repo) {
|
|
|
241
260
|
process.stderr.write(`${NAME}: could not remove the throwaway worktree ${wt}: ${e.message}\n`);
|
|
242
261
|
}
|
|
243
262
|
}
|
|
244
|
-
|
|
263
|
+
tryGit(["worktree", "prune"]);
|
|
245
264
|
}
|
|
246
265
|
|
|
247
266
|
function main(argv) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { test } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
4
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { tempDir } from "./temp-dir.mjs";
|
|
7
7
|
import { writeExecStub } from "./exec-stub.mjs";
|
|
@@ -371,13 +371,75 @@ test("a proof that cannot be attempted exits 2, distinct from a Recipe that is n
|
|
|
371
371
|
"a refusal at the argument boundary creates no worktree");
|
|
372
372
|
});
|
|
373
373
|
|
|
374
|
+
// Every temp dir the proof made, scanned for the throwaway worktree it names `wt`.
|
|
375
|
+
function assertNoWorktreeLeft(tmp) {
|
|
376
|
+
for (const d of readdirSync(tmp)) {
|
|
377
|
+
assert.deepEqual(readdirSync(join(tmp, d)).filter((n) => n === "wt"), [], `${d} still holds the throwaway worktree`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
374
381
|
test("the proof leaves no worktree directory behind in its temp dir", () => {
|
|
375
382
|
const { dir } = repo(MAVEN_FILES);
|
|
376
383
|
const r = prove(dir, MAVEN_PROOF);
|
|
377
384
|
assert.equal(r.status, 0, r.err);
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
385
|
+
assertNoWorktreeLeft(r.tmp);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("a git that cannot be started is named as such, not reported as a missing repository", () => {
|
|
389
|
+
const { dir } = repo(MAVEN_FILES);
|
|
390
|
+
const r = prove(dir, MAVEN_PROOF, { env: { PATH: "/nonexistent" } });
|
|
391
|
+
assert.equal(r.status, 2, r.err);
|
|
392
|
+
assert.match(r.err, /^recipe-prove: could not start git: .*ENOENT/);
|
|
393
|
+
assert.doesNotMatch(r.err, /not a git repository/);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// A PATH holding only `sh` and a `git` that execs the real one, so the Install
|
|
397
|
+
// step can take git away by deleting it: every later git call then
|
|
398
|
+
// fails to start, in the proof and in the worktree cleanup alike.
|
|
399
|
+
test("a git that stops starting during the Install step is no verdict, never NOT PROVEN — and the worktree is still removed", () => {
|
|
400
|
+
const realGit = execFileSync("sh", ["-c", "command -v git"], { encoding: "utf8" }).trim();
|
|
401
|
+
const { dir } = repo(MAVEN_FILES);
|
|
402
|
+
const bin = tempDir("recipe-prove-path-");
|
|
403
|
+
writeExecStub(join(bin, "git"), `#!/bin/sh\nexec '${realGit}' "$@"\n`);
|
|
404
|
+
symlinkSync("/bin/sh", join(bin, "sh"));
|
|
405
|
+
const install = `'${process.execPath}' -e 'require("fs").rmSync(process.argv[1])' '${join(bin, "git")}'`;
|
|
406
|
+
const r = prove(dir, ["--install", install, "--test", "true", "--count-line", "x 1", "--test-count", "1"], { env: { PATH: bin } });
|
|
407
|
+
assert.equal(r.status, 2, r.err);
|
|
408
|
+
assert.match(r.err, /could not start git: .*ENOENT/);
|
|
409
|
+
assert.match(r.err, /skipped git worktree remove/);
|
|
410
|
+
assert.doesNotMatch(r.err, /NOT PROVEN/);
|
|
411
|
+
assertNoWorktreeLeft(r.tmp);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// A git that started and ran is never "could not start": here its status
|
|
415
|
+
// output outgrows spawnSync's default 1 MiB buffer, which kills it and sets
|
|
416
|
+
// `error` (ENOBUFS) on a process that did run. The tree the Install step left
|
|
417
|
+
// is unreadable, so the proof does not hold — NOT PROVEN, exit 1.
|
|
418
|
+
test("a git whose output outgrows the spawn buffer is a failed command, not a git that could not be started", () => {
|
|
419
|
+
const { dir } = repo(MAVEN_FILES);
|
|
420
|
+
const flood = 'const fs = require("fs"); for (let i = 0; i < 7000; i++) fs.writeFileSync("f".repeat(200) + i, "")';
|
|
421
|
+
const install = `'${process.execPath}' -e '${flood}'`;
|
|
422
|
+
const r = prove(dir, ["--install", install, "--test", "true", "--count-line", "x 1", "--test-count", "1"]);
|
|
423
|
+
assert.equal(r.status, 1, r.err);
|
|
424
|
+
assert.match(r.err, /NOT PROVEN — could not read the tree state in the throwaway worktree/);
|
|
425
|
+
assert.doesNotMatch(r.err, /could not start git/);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// A git that starts for the worktree removal and not for the prune after it:
|
|
429
|
+
// the stub deletes itself once `worktree remove` has run. The proof's own
|
|
430
|
+
// outcome is NOT PROVEN (the Install step fails), and the prune that cannot
|
|
431
|
+
// start must not replace it.
|
|
432
|
+
test("a git that stops starting during cleanup does not replace the proof's own NOT PROVEN", () => {
|
|
433
|
+
const realGit = execFileSync("sh", ["-c", "command -v git"], { encoding: "utf8" }).trim();
|
|
434
|
+
const { dir } = repo(MAVEN_FILES);
|
|
435
|
+
const bin = tempDir("recipe-prove-path-");
|
|
436
|
+
const stub = join(bin, "git");
|
|
437
|
+
writeExecStub(stub, `#!/bin/sh\n'${realGit}' "$@"\nrc=$?\nif [ "$2" = remove ]; then /bin/rm -f '${stub}'; fi\nexit $rc\n`);
|
|
438
|
+
symlinkSync("/bin/sh", join(bin, "sh"));
|
|
439
|
+
const r = prove(dir, ["--install", "false", "--test", "true", "--count-line", "x 1", "--test-count", "1"], { env: { PATH: bin } });
|
|
440
|
+
assert.equal(r.status, 1, r.err);
|
|
441
|
+
assert.match(r.err, /NOT PROVEN/);
|
|
442
|
+
assert.match(r.err, /skipped git worktree prune: could not start git/);
|
|
381
443
|
});
|
|
382
444
|
|
|
383
445
|
// Whether the process ignores file modes: root writes into a 0555 directory.
|
|
@@ -393,9 +455,7 @@ test("read-only install output cannot turn a proof that held into a crash", { sk
|
|
|
393
455
|
const r = prove(dir, ["--install", READ_ONLY_INSTALL, ...MAVEN_PROOF.slice(2)]);
|
|
394
456
|
assert.equal(r.status, 0, r.err);
|
|
395
457
|
assert.equal(JSON.parse(readFileSync(cachePath(dir), "utf8")).derivedAt, head);
|
|
396
|
-
|
|
397
|
-
assert.deepEqual(readdirSync(join(r.tmp, d)).filter((n) => n === "wt"), [], `${d} still holds the throwaway worktree`);
|
|
398
|
-
}
|
|
458
|
+
assertNoWorktreeLeft(r.tmp);
|
|
399
459
|
assert.equal(git(dir, "worktree", "list", "--porcelain").split("\n").filter((l) => l.startsWith("worktree ")).length, 1);
|
|
400
460
|
});
|
|
401
461
|
|