@feigi/fleet-ctl 3.21.4 → 3.21.5
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
|
@@ -38,16 +38,18 @@
|
|
|
38
38
|
// — <workspace> being the directory holding the repository's common git dir,
|
|
39
39
|
// the place derive-testcmd.sh reads it from — and then read back through
|
|
40
40
|
// derive-testcmd.sh for both fields. A cache the reader refuses is rolled
|
|
41
|
-
// back to whatever stood before,
|
|
42
|
-
// what a usable cache is. A failed
|
|
41
|
+
// back to whatever stood before, as is one whose read-back is no verdict, so
|
|
42
|
+
// the reader stays the one authority on what a usable cache is. A failed
|
|
43
|
+
// proof never touches an existing cache.
|
|
43
44
|
//
|
|
44
45
|
// Exit 0: proven, cache written (its path and contents on stdout).
|
|
45
46
|
// Exit 1: NOT PROVEN — no cache written; the reason on stderr.
|
|
46
47
|
// Exit 2: no verdict on the proof — it could not be attempted (usage, not a
|
|
47
48
|
// repository, origin/main missing, the worktree could not be made), or
|
|
48
49
|
// a git or filesystem fault stopped it before the cache was settled
|
|
49
|
-
// (a git command could not be started
|
|
50
|
-
//
|
|
50
|
+
// (a git command could not be started, the reader's own git
|
|
51
|
+
// included, or the reader's `sh`; the cache or its temp file could
|
|
52
|
+
// not be written).
|
|
51
53
|
|
|
52
54
|
import { spawnSync } from "node:child_process";
|
|
53
55
|
import { closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -212,7 +214,11 @@ function prove(o, wt, logs) {
|
|
|
212
214
|
}
|
|
213
215
|
|
|
214
216
|
// Write, then read back through the one reader. A cache it refuses is undone:
|
|
215
|
-
// the prior bytes restored, or the file removed when none stood before.
|
|
217
|
+
// the prior bytes restored, or the file removed when none stood before. The
|
|
218
|
+
// cache is undone just the same when the read-back is no verdict: an `sh` that
|
|
219
|
+
// could not be started read nothing, and the reader runs git itself and reads
|
|
220
|
+
// a git that will not start as no repository at all, so its refusal is a
|
|
221
|
+
// verdict on what was proven only while git still starts.
|
|
216
222
|
function writeCache(cache, recipe, repo) {
|
|
217
223
|
mkdirSync(dirname(cache), { recursive: true });
|
|
218
224
|
const prior = existsSync(cache) ? readFileSync(cache) : null;
|
|
@@ -226,11 +232,17 @@ function writeCache(cache, recipe, repo) {
|
|
|
226
232
|
}
|
|
227
233
|
for (const field of ["install", "test"]) {
|
|
228
234
|
const r = spawnSync("sh", [READER, repo, field], { env: ENV, encoding: "utf8" });
|
|
229
|
-
if (r.status
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
235
|
+
if (r.status === 0 && r.stdout.replace(/\n$/, "") === recipe[field]) continue;
|
|
236
|
+
if (prior === null) unlinkSync(cache);
|
|
237
|
+
else writeFileSync(cache, prior);
|
|
238
|
+
if (r.error && !r.pid) throw cannot(`could not start sh to read the Recipe cache back: ${r.error.message}`);
|
|
239
|
+
try {
|
|
240
|
+
git(["--version"]);
|
|
241
|
+
} catch (e) {
|
|
242
|
+
if (!(e instanceof Refusal)) throw e;
|
|
243
|
+
throw cannot(`${e.message} — the Recipe cache reader runs git, so its refusal is no verdict on what was proven`);
|
|
233
244
|
}
|
|
245
|
+
throw notProven(`the Recipe cache reader refuses what was proven: ${(r.stderr || r.stdout).trim()}`);
|
|
234
246
|
}
|
|
235
247
|
}
|
|
236
248
|
|
|
@@ -442,6 +442,54 @@ test("a git that stops starting during cleanup does not replace the proof's own
|
|
|
442
442
|
assert.match(r.err, /skipped git worktree prune: could not start git/);
|
|
443
443
|
});
|
|
444
444
|
|
|
445
|
+
// A PATH holding exactly what the proof and the Recipe cache reader run — `sh`,
|
|
446
|
+
// a `git` that execs the real one, `node`, and the reader's `mktemp`, `cat`
|
|
447
|
+
// and `rm` — so the Test entrypoint can take git away by deleting it. With the
|
|
448
|
+
// count proof no proof step runs git after the Test entrypoint, so the first
|
|
449
|
+
// thing to meet the missing git is the reader, during the read-back.
|
|
450
|
+
function readerPath() {
|
|
451
|
+
const realGit = execFileSync("sh", ["-c", "command -v git"], { encoding: "utf8" }).trim();
|
|
452
|
+
const bin = tempDir("recipe-prove-path-");
|
|
453
|
+
writeExecStub(join(bin, "git"), `#!/bin/sh\nexec '${realGit}' "$@"\n`);
|
|
454
|
+
symlinkSync("/bin/sh", join(bin, "sh"));
|
|
455
|
+
symlinkSync(process.execPath, join(bin, "node"));
|
|
456
|
+
for (const tool of ["mktemp", "cat", "rm"]) {
|
|
457
|
+
symlinkSync(execFileSync("sh", ["-c", `command -v ${tool}`], { encoding: "utf8" }).trim(), join(bin, tool));
|
|
458
|
+
}
|
|
459
|
+
return bin;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
test("a git that stops starting before the cache is read back is no verdict, never NOT PROVEN — and the prior cache is restored", () => {
|
|
463
|
+
// The control: the same PATH with git left in place is a proof that holds.
|
|
464
|
+
const kept = repo(MAVEN_FILES);
|
|
465
|
+
const ok = prove(kept.dir, ["--install", "true", "--test", "echo 'tests 1'", "--count-line", "tests 1", "--test-count", "1"], { env: { PATH: readerPath() } });
|
|
466
|
+
assert.equal(ok.status, 0, ok.err);
|
|
467
|
+
assert.match(ok.out, /^recipe-prove: PROVEN/);
|
|
468
|
+
|
|
469
|
+
const { dir } = repo(MAVEN_FILES);
|
|
470
|
+
mkdirSync(join(dir, ".fleet"));
|
|
471
|
+
writeFileSync(cachePath(dir), "prior bytes");
|
|
472
|
+
const bin = readerPath();
|
|
473
|
+
const r = prove(dir, ["--install", "true", "--test", `rm '${join(bin, "git")}'; echo 'tests 1'`, "--count-line", "tests 1", "--test-count", "1"], { env: { PATH: bin } });
|
|
474
|
+
assert.equal(r.status, 2, r.err);
|
|
475
|
+
assert.match(r.err, /^recipe-prove: could not start git: .*ENOENT — the Recipe cache reader runs git, so its refusal is no verdict/m);
|
|
476
|
+
assert.doesNotMatch(r.err, /NOT PROVEN/);
|
|
477
|
+
assert.doesNotMatch(r.err, /not a git repository/);
|
|
478
|
+
assert.equal(readFileSync(cachePath(dir), "utf8"), "prior bytes", "a cache the reader never settled is rolled back");
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// The reader's own `sh`, taken away the same way: a read-back that never ran
|
|
482
|
+
// read nothing, so it is no verdict either.
|
|
483
|
+
test("an sh that stops starting before the cache is read back is no verdict, never NOT PROVEN — and no cache is left", () => {
|
|
484
|
+
const { dir } = repo(MAVEN_FILES);
|
|
485
|
+
const bin = readerPath();
|
|
486
|
+
const r = prove(dir, ["--install", "true", "--test", `rm '${join(bin, "sh")}'; echo 'tests 1'`, "--count-line", "tests 1", "--test-count", "1"], { env: { PATH: bin } });
|
|
487
|
+
assert.equal(r.status, 2, r.err);
|
|
488
|
+
assert.match(r.err, /^recipe-prove: could not start sh to read the Recipe cache back: .*ENOENT/m);
|
|
489
|
+
assert.doesNotMatch(r.err, /NOT PROVEN/);
|
|
490
|
+
assert.equal(existsSync(cachePath(dir)), false, "a cache the reader never read is rolled back");
|
|
491
|
+
});
|
|
492
|
+
|
|
445
493
|
// Whether the process ignores file modes: root writes into a 0555 directory.
|
|
446
494
|
const IGNORES_MODES = process.getuid?.() === 0 && "root ignores file modes";
|
|
447
495
|
|