mandrel-platform 1.13.0 → 1.13.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/README.md +6 -2
- package/package.json +4 -2
- package/scripts/audit-check.mjs +210 -47
- package/scripts/audit-check.test.mjs +479 -49
- package/scripts/check-action-download-retries.mjs +155 -11
- package/scripts/check-action-download-retries.test.mjs +168 -3
- package/scripts/check-advisory-scan-setup.test.mjs +186 -5
- package/scripts/check-coverage-threshold.mjs +112 -17
- package/scripts/check-coverage-threshold.test.mjs +335 -0
- package/scripts/check-husky-hook-modes.test.mjs +134 -0
- package/scripts/check-runner-runs-on.test.mjs +223 -2
- package/scripts/check-semgrep-lockfile.test.mjs +238 -7
- package/scripts/check-workflow-lint-tier.test.mjs +176 -0
- package/scripts/check-workflow-portability.mjs +6 -3
- package/scripts/check-workflow-portability.test.mjs +1 -1
- package/scripts/env-doctor.mjs +493 -62
- package/scripts/env-doctor.test.mjs +529 -3
- package/scripts/fixtures/npm-audit-v2.json +66 -0
- package/scripts/install-git-hooks.mjs +123 -0
- package/scripts/install-git-hooks.test.mjs +160 -0
- package/scripts/issue-intake.test.mjs +351 -21
- package/scripts/runner-toggle.test.mjs +407 -0
- package/scripts/select-semgrep-python.sh +98 -17
- package/scripts/select-semgrep-python.test.mjs +139 -12
- package/scripts/update-semgrep-rules.mjs +83 -5
- package/scripts/update-semgrep-rules.test.mjs +55 -1
- package/scripts/workflow-lint-gate.test.mjs +128 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* install-git-hooks.mjs — point git at this repository's tracked hooks.
|
|
4
|
+
*
|
|
5
|
+
* Wired into `prepare`, so a plain `npm install` leaves a fresh clone with its
|
|
6
|
+
* hooks running. Before this existed nothing in the repository set
|
|
7
|
+
* `core.hooksPath` at all — no husky dependency, no bootstrap step — so git
|
|
8
|
+
* looked in `.git/hooks`, found nothing, and committed normally. The `.husky/`
|
|
9
|
+
* gate was unreachable in every clone no matter what mode its files carried,
|
|
10
|
+
* which is the second half of the defect Story #511 fixed the first half of.
|
|
11
|
+
*
|
|
12
|
+
* ## Why the value is relative
|
|
13
|
+
*
|
|
14
|
+
* `core.hooksPath` is resolved by git against **each working tree's own root**,
|
|
15
|
+
* not against the common git dir. That cuts both ways:
|
|
16
|
+
*
|
|
17
|
+
* - An **absolute** path works identically from every working tree, but it
|
|
18
|
+
* names one machine's checkout, so it can never be committed — which is
|
|
19
|
+
* exactly how this repository ended up depending on a `git config` someone
|
|
20
|
+
* ran by hand once.
|
|
21
|
+
* - A **relative** `.husky` is committable, and here it is also correct from
|
|
22
|
+
* a linked worktree, because `.husky/*` is **tracked**: every worktree
|
|
23
|
+
* checks the hooks out for itself. The hazard the framework's
|
|
24
|
+
* `.agents/scripts/lib/worktree/git-hooks.js` warns about — a hooks path
|
|
25
|
+
* resolving to a directory that is not there — comes from husky's
|
|
26
|
+
* *generated, self-ignored* `.husky/_` shim, which this repository does not
|
|
27
|
+
* use.
|
|
28
|
+
*
|
|
29
|
+
* So the relative form is the one that can live in git, and tracking the hooks
|
|
30
|
+
* is what makes it safe.
|
|
31
|
+
*
|
|
32
|
+
* ## Interaction with the worktree hook provisioner
|
|
33
|
+
*
|
|
34
|
+
* Moving off an absolute value changes which branch
|
|
35
|
+
* `.agents/scripts/lib/worktree/git-hooks.js` takes: it no longer short-circuits
|
|
36
|
+
* on `hooks-path-absolute`, it copies the main checkout's `.husky` into the
|
|
37
|
+
* linked worktree — deliberately "refreshing rather than preserving", so it
|
|
38
|
+
* REPLACES whatever is there. At worktree creation that is a no-op, because the
|
|
39
|
+
* worktree was just seeded from the same base. It bites only when that
|
|
40
|
+
* provisioner is re-run by hand inside a worktree whose branch adds or edits a
|
|
41
|
+
* hook the base branch does not carry yet: the working-tree copy is discarded
|
|
42
|
+
* and is restored with `git checkout -- .husky/<hook>` (the commit is
|
|
43
|
+
* untouched). The trade is deliberate — the payoff is that a worktree is gated
|
|
44
|
+
* by the hooks ITS OWN branch tracks, which is what lets a change to a hook be
|
|
45
|
+
* tested on the branch making it.
|
|
46
|
+
*
|
|
47
|
+
* ## Scope of the write
|
|
48
|
+
*
|
|
49
|
+
* The value is written at **local** scope (`.git/config`), which linked
|
|
50
|
+
* worktrees share, so one install covers the whole checkout. A pre-existing
|
|
51
|
+
* **worktree-scoped** override still wins over local scope by design; clear one
|
|
52
|
+
* with `git config --worktree --unset core.hooksPath` if a worktree was pinned
|
|
53
|
+
* to some other path.
|
|
54
|
+
*
|
|
55
|
+
* Idempotent, and deliberately total about doing nothing: an install that is
|
|
56
|
+
* not inside a git work tree (an npm dependency unpacked from a tarball, a
|
|
57
|
+
* source archive) reports a skip and exits 0 rather than failing someone's
|
|
58
|
+
* install over a hook.
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
import fs from "node:fs";
|
|
62
|
+
import path from "node:path";
|
|
63
|
+
import { spawnSync as defaultSpawnSync } from "node:child_process";
|
|
64
|
+
import { isDirectInvocation } from "./lib/entry-guard.mjs";
|
|
65
|
+
|
|
66
|
+
/** The committed hooks directory, relative to the working-tree root. */
|
|
67
|
+
export const HOOKS_PATH = ".husky";
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Ensure `core.hooksPath` points at {@link HOOKS_PATH}.
|
|
71
|
+
*
|
|
72
|
+
* @param {object} [opts]
|
|
73
|
+
* @param {string} [opts.cwd] Directory to run git in.
|
|
74
|
+
* @param {typeof fs} [opts.fsImpl] Filesystem seam.
|
|
75
|
+
* @param {typeof defaultSpawnSync} [opts.spawnImpl] Child-process seam.
|
|
76
|
+
* @returns {{ action: 'set'|'replaced'|'already-set'|'skipped',
|
|
77
|
+
* reason?: string, previous?: string|null, hooksPath: string|null }}
|
|
78
|
+
* @throws {Error} when git refuses the write — a silent failure here would
|
|
79
|
+
* restore the exact "gate present, never runs" state this script exists to
|
|
80
|
+
* end.
|
|
81
|
+
*/
|
|
82
|
+
export function installGitHooks({
|
|
83
|
+
cwd = process.cwd(),
|
|
84
|
+
fsImpl = fs,
|
|
85
|
+
spawnImpl = defaultSpawnSync,
|
|
86
|
+
} = {}) {
|
|
87
|
+
const git = (...args) => spawnImpl("git", args, { cwd, encoding: "utf8" });
|
|
88
|
+
|
|
89
|
+
const inside = git("rev-parse", "--is-inside-work-tree");
|
|
90
|
+
if (inside.status !== 0 || (inside.stdout ?? "").trim() !== "true") {
|
|
91
|
+
return { action: "skipped", reason: "not-a-git-work-tree", hooksPath: null };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!fsImpl.existsSync(path.resolve(cwd, HOOKS_PATH))) {
|
|
95
|
+
return { action: "skipped", reason: "hooks-dir-absent", hooksPath: null };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const read = git("config", "--get", "core.hooksPath");
|
|
99
|
+
// `--get` exits non-zero when the key is unset, which is the normal state of
|
|
100
|
+
// a fresh clone rather than an error.
|
|
101
|
+
const current = read.status === 0 ? (read.stdout ?? "").trim() : "";
|
|
102
|
+
if (current === HOOKS_PATH) {
|
|
103
|
+
return { action: "already-set", hooksPath: HOOKS_PATH };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const write = git("config", "core.hooksPath", HOOKS_PATH);
|
|
107
|
+
if (write.status !== 0) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`install-git-hooks: could not set core.hooksPath: ${(write.stderr ?? "").trim() || `git exited ${write.status}`}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
action: current ? "replaced" : "set",
|
|
115
|
+
previous: current || null,
|
|
116
|
+
hooksPath: HOOKS_PATH,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (isDirectInvocation(import.meta.url)) {
|
|
121
|
+
const result = installGitHooks();
|
|
122
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
123
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* install-git-hooks.test.mjs — the `core.hooksPath` wiring, and the two places
|
|
4
|
+
* that wiring can silently fall out of the repository again (Story #513).
|
|
5
|
+
*
|
|
6
|
+
* The bug this pins: nothing set `core.hooksPath`. There was no husky
|
|
7
|
+
* dependency, no `prepare` step, and no bootstrap hook — the value on a
|
|
8
|
+
* maintainer's machine was a `git config` run by hand at some forgotten point.
|
|
9
|
+
* Git therefore looked in `.git/hooks` in every fresh clone, found nothing, and
|
|
10
|
+
* committed normally. Story #511 had just made `.husky/pre-commit` executable;
|
|
11
|
+
* this is the other half, and without it that mode bit buys nothing.
|
|
12
|
+
*
|
|
13
|
+
* Like the mode bug, the failure is silent: no hook runs, no warning is
|
|
14
|
+
* printed, `git commit` exits 0. So the assertions below cover both the
|
|
15
|
+
* function's decisions AND the repository wiring that invokes it — a correct
|
|
16
|
+
* installer that `prepare` no longer calls is the same silent no-gate outcome
|
|
17
|
+
* by a different route.
|
|
18
|
+
*
|
|
19
|
+
* Seams rather than a real repo: the function takes `fsImpl` / `spawnImpl`, so
|
|
20
|
+
* every branch is exercised without spawning git or writing to a real config.
|
|
21
|
+
* Stubs are plain objects passed through the parameter, per
|
|
22
|
+
* `.agents/rules/test-seams.md`.
|
|
23
|
+
*
|
|
24
|
+
* Run: node --test scripts/install-git-hooks.test.mjs
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import assert from "node:assert/strict";
|
|
28
|
+
import { test } from "node:test";
|
|
29
|
+
import { readFileSync } from "node:fs";
|
|
30
|
+
import { installGitHooks, HOOKS_PATH } from "./install-git-hooks.mjs";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A `spawnSync` stub that records calls and answers each git subcommand from
|
|
34
|
+
* `replies`, keyed by the subcommand words joined with a space.
|
|
35
|
+
*/
|
|
36
|
+
function gitStub(replies) {
|
|
37
|
+
const calls = [];
|
|
38
|
+
const spawnImpl = (_bin, args) => {
|
|
39
|
+
calls.push(args);
|
|
40
|
+
for (const [key, reply] of Object.entries(replies)) {
|
|
41
|
+
if (args.join(" ").startsWith(key)) return reply;
|
|
42
|
+
}
|
|
43
|
+
return { status: 0, stdout: "", stderr: "" };
|
|
44
|
+
};
|
|
45
|
+
return { spawnImpl, calls };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const OK_WORKTREE = { "rev-parse": { status: 0, stdout: "true\n", stderr: "" } };
|
|
49
|
+
const HOOKS_DIR_PRESENT = { existsSync: () => true };
|
|
50
|
+
|
|
51
|
+
/** Did the stub attempt to WRITE the config (as opposed to reading it)? */
|
|
52
|
+
function wroteConfig(calls) {
|
|
53
|
+
return calls.some(
|
|
54
|
+
(a) => a[0] === "config" && !a.includes("--get") && a[1] === "core.hooksPath",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
test("outside a git work tree it skips instead of failing the install", () => {
|
|
59
|
+
// An npm dependency unpacked from a tarball is not a repo. Failing here would
|
|
60
|
+
// break someone's `npm install` over a hook they never asked for.
|
|
61
|
+
const { spawnImpl, calls } = gitStub({
|
|
62
|
+
"rev-parse": { status: 128, stdout: "", stderr: "not a git repository" },
|
|
63
|
+
});
|
|
64
|
+
const res = installGitHooks({ spawnImpl, fsImpl: HOOKS_DIR_PRESENT });
|
|
65
|
+
|
|
66
|
+
assert.equal(res.action, "skipped");
|
|
67
|
+
assert.equal(res.reason, "not-a-git-work-tree");
|
|
68
|
+
assert.equal(wroteConfig(calls), false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("with no .husky directory it skips rather than pointing git at nothing", () => {
|
|
72
|
+
const { spawnImpl, calls } = gitStub(OK_WORKTREE);
|
|
73
|
+
const res = installGitHooks({ spawnImpl, fsImpl: { existsSync: () => false } });
|
|
74
|
+
|
|
75
|
+
assert.equal(res.action, "skipped");
|
|
76
|
+
assert.equal(res.reason, "hooks-dir-absent");
|
|
77
|
+
assert.equal(wroteConfig(calls), false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("an unset core.hooksPath is set to the tracked hooks directory", () => {
|
|
81
|
+
// `git config --get` exits non-zero when the key is unset — the normal state
|
|
82
|
+
// of a fresh clone, and the exact case that left the gate unreachable.
|
|
83
|
+
const { spawnImpl, calls } = gitStub({
|
|
84
|
+
...OK_WORKTREE,
|
|
85
|
+
"config --get": { status: 1, stdout: "", stderr: "" },
|
|
86
|
+
});
|
|
87
|
+
const res = installGitHooks({ spawnImpl, fsImpl: HOOKS_DIR_PRESENT });
|
|
88
|
+
|
|
89
|
+
assert.equal(res.action, "set");
|
|
90
|
+
assert.equal(res.previous, null);
|
|
91
|
+
assert.equal(res.hooksPath, HOOKS_PATH);
|
|
92
|
+
assert.deepEqual(
|
|
93
|
+
calls.find((a) => a[0] === "config" && !a.includes("--get")),
|
|
94
|
+
["config", "core.hooksPath", HOOKS_PATH],
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("a machine-specific absolute value is replaced, and reported", () => {
|
|
99
|
+
// This is what the repository actually carried: an absolute path naming one
|
|
100
|
+
// developer's checkout, which no other clone could ever have.
|
|
101
|
+
const { spawnImpl } = gitStub({
|
|
102
|
+
...OK_WORKTREE,
|
|
103
|
+
"config --get": { status: 0, stdout: "/Users/someone/repo/.husky\n", stderr: "" },
|
|
104
|
+
});
|
|
105
|
+
const res = installGitHooks({ spawnImpl, fsImpl: HOOKS_DIR_PRESENT });
|
|
106
|
+
|
|
107
|
+
assert.equal(res.action, "replaced");
|
|
108
|
+
assert.equal(res.previous, "/Users/someone/repo/.husky");
|
|
109
|
+
assert.equal(res.hooksPath, HOOKS_PATH);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("an already-correct value is left alone (idempotent re-install)", () => {
|
|
113
|
+
const { spawnImpl, calls } = gitStub({
|
|
114
|
+
...OK_WORKTREE,
|
|
115
|
+
"config --get": { status: 0, stdout: `${HOOKS_PATH}\n`, stderr: "" },
|
|
116
|
+
});
|
|
117
|
+
const res = installGitHooks({ spawnImpl, fsImpl: HOOKS_DIR_PRESENT });
|
|
118
|
+
|
|
119
|
+
assert.equal(res.action, "already-set");
|
|
120
|
+
assert.equal(wroteConfig(calls), false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("a refused config write throws instead of reporting success", () => {
|
|
124
|
+
// Swallowing this would restore the precise failure mode being fixed: the
|
|
125
|
+
// gate present, believed wired, and never running.
|
|
126
|
+
const { spawnImpl } = gitStub({
|
|
127
|
+
...OK_WORKTREE,
|
|
128
|
+
"config --get": { status: 1, stdout: "", stderr: "" },
|
|
129
|
+
"config core.hooksPath": { status: 4, stdout: "", stderr: "could not lock config file" },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
assert.throws(
|
|
133
|
+
() => installGitHooks({ spawnImpl, fsImpl: HOOKS_DIR_PRESENT }),
|
|
134
|
+
/could not lock config file/,
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("`prepare` actually invokes the installer", () => {
|
|
139
|
+
// The installer only runs because npm runs it. If this call is dropped from
|
|
140
|
+
// `prepare`, every assertion above still passes and no clone is wired.
|
|
141
|
+
const pkg = JSON.parse(readFileSync("package.json", "utf8"));
|
|
142
|
+
assert.match(
|
|
143
|
+
pkg.scripts?.prepare ?? "",
|
|
144
|
+
/install-git-hooks\.mjs/,
|
|
145
|
+
"package.json `prepare` must run scripts/install-git-hooks.mjs, or a fresh clone gets no hooks",
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("the commit-msg hook exists and runs commitlint", () => {
|
|
150
|
+
// `rules/git-conventions.md` promises this gate. Its mode is asserted by
|
|
151
|
+
// check-husky-hook-modes.test.mjs; what it RUNS is asserted here.
|
|
152
|
+
const hook = readFileSync(".husky/commit-msg", "utf8");
|
|
153
|
+
assert.match(hook, /commitlint/, ".husky/commit-msg must invoke commitlint");
|
|
154
|
+
|
|
155
|
+
const pkg = JSON.parse(readFileSync("package.json", "utf8"));
|
|
156
|
+
assert.ok(
|
|
157
|
+
pkg.devDependencies?.["@commitlint/cli"],
|
|
158
|
+
"@commitlint/cli must be a devDependency — the hook runs it with --no-install",
|
|
159
|
+
);
|
|
160
|
+
});
|