@zgeoff/chex 0.1.9 → 0.2.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
CHANGED
|
@@ -94,7 +94,6 @@ extends:
|
|
|
94
94
|
- node_modules/@zgeoff/chex/lefthook.yml
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
The baseline's jobs call `chex <check
|
|
98
|
-
|
|
99
|
-
`format:check` and `typecheck` scripts.
|
|
97
|
+
The baseline's jobs call `chex <check>`, `bun run check:docs`, `rig related` and the repo's own
|
|
98
|
+
`format:check` and `typecheck` scripts. Each job's name starts with `chex-`.
|
|
100
99
|
[The pre-push baseline](../../docs/architecture/pre-push-baseline.md) describes each job.
|
package/lefthook.yml
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
# also runs typecheck before a commit that changes code or config, after the
|
|
9
9
|
# repo's own pre-commit fixers. A repo adds its own jobs under
|
|
10
10
|
# pre-commit.jobs and pre-push.jobs in lefthook.yml; they run before these.
|
|
11
|
+
# lefthook 2.2.2 lets a job of an extended file replace the job of the same
|
|
12
|
+
# name in lefthook.yml, so each job name here starts with chex-.
|
|
11
13
|
# Each job runs under diet's memory cap, so a run that outgrows the host fails
|
|
12
14
|
# alone, with its peak memory in the output. bun x --no-install runs the
|
|
13
15
|
# repo's own install of chex, diet and rig, and fails when one is missing
|
|
@@ -28,7 +30,7 @@ pre-commit:
|
|
|
28
30
|
piped: true
|
|
29
31
|
jobs:
|
|
30
32
|
# lefthook skips the job when no staged file matches the glob.
|
|
31
|
-
- name: typecheck
|
|
33
|
+
- name: chex-typecheck
|
|
32
34
|
glob: '*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,json,jsonc}'
|
|
33
35
|
run: '{clear-git-env} bun x --no-install diet -- bun run typecheck'
|
|
34
36
|
|
|
@@ -39,13 +41,13 @@ pre-push:
|
|
|
39
41
|
# The jobs after this one read the files on disk, so this one fails the
|
|
40
42
|
# push when a file the branch changes or deletes holds a change that is
|
|
41
43
|
# not committed, and names each such file.
|
|
42
|
-
- name: uncommitted
|
|
44
|
+
- name: chex-uncommitted
|
|
43
45
|
run: '{clear-git-env} bun x --no-install diet -- bun x --no-install chex uncommitted'
|
|
44
|
-
- name: format:check
|
|
46
|
+
- name: chex-format:check
|
|
45
47
|
run: '{clear-git-env} bun x --no-install diet -- bun run format:check'
|
|
46
48
|
# Plain oxlint, without the unused-directive check: a directive for a
|
|
47
49
|
# type-aware rule reads as unused in plain mode.
|
|
48
|
-
- name: lint
|
|
50
|
+
- name: chex-lint
|
|
49
51
|
files:
|
|
50
52
|
unset $(git rev-parse --local-env-vars); bun
|
|
51
53
|
node_modules/@zgeoff/chex/src/read-branch-files.ts
|
|
@@ -57,7 +59,7 @@ pre-push:
|
|
|
57
59
|
# glob, so a file the branch leaves unchanged never fails a push. A check
|
|
58
60
|
# whose program the machine lacks prints a notice and passes: CI installs
|
|
59
61
|
# the program and runs the check on every pull request.
|
|
60
|
-
- name: shell
|
|
62
|
+
- name: chex-shell
|
|
61
63
|
files:
|
|
62
64
|
unset $(git rev-parse --local-env-vars); bun
|
|
63
65
|
node_modules/@zgeoff/chex/src/read-branch-files.ts
|
|
@@ -77,7 +79,7 @@ pre-push:
|
|
|
77
79
|
# The docs checks pick the docs they read themselves, so the glob only
|
|
78
80
|
# decides whether the job runs. The repo's check:docs script names the
|
|
79
81
|
# checks it takes, such as chex prose length links.
|
|
80
|
-
- name: docs
|
|
82
|
+
- name: chex-docs
|
|
81
83
|
files:
|
|
82
84
|
unset $(git rev-parse --local-env-vars); bun
|
|
83
85
|
node_modules/@zgeoff/chex/src/read-branch-files.ts
|
|
@@ -89,7 +91,7 @@ pre-push:
|
|
|
89
91
|
# rhysd/actionlint#654 ships; the ignore lapses on the same date as in CI.
|
|
90
92
|
# The workflow lint runs after actionlint, even when actionlint is missing
|
|
91
93
|
# or fails.
|
|
92
|
-
- name: workflows
|
|
94
|
+
- name: chex-workflows
|
|
93
95
|
files:
|
|
94
96
|
unset $(git rev-parse --local-env-vars); bun
|
|
95
97
|
node_modules/@zgeoff/chex/src/read-branch-files.ts
|
|
@@ -116,28 +118,28 @@ pre-push:
|
|
|
116
118
|
# each job lists package.json for {files}, inside a shell comment the
|
|
117
119
|
# check never sees: every repo that takes the baseline has the file, as
|
|
118
120
|
# it installs chex, and lefthook drops a listed path that does not exist.
|
|
119
|
-
- name: commits
|
|
121
|
+
- name: chex-commits
|
|
120
122
|
files: echo package.json
|
|
121
123
|
use_stdin: true
|
|
122
124
|
run:
|
|
123
125
|
'{clear-git-env} bun x --no-install diet -- bun x --no-install chex commits --pushed-refs #
|
|
124
126
|
{files}'
|
|
125
127
|
# package.json keeps this job from the same lefthook skip.
|
|
126
|
-
- name: local-paths
|
|
128
|
+
- name: chex-local-paths
|
|
127
129
|
files: echo package.json
|
|
128
130
|
use_stdin: true
|
|
129
131
|
run:
|
|
130
132
|
'{clear-git-env} bun x --no-install diet -- bun x --no-install chex local-paths
|
|
131
133
|
--pushed-refs # {files}'
|
|
132
|
-
- name: test-timeouts
|
|
134
|
+
- name: chex-test-timeouts
|
|
133
135
|
run: '{clear-git-env} bun x --no-install diet -- bun x --no-install chex test-timeouts'
|
|
134
|
-
- name: dependency-gates
|
|
136
|
+
- name: chex-dependency-gates
|
|
135
137
|
run: '{clear-git-env} bun x --no-install diet -- bun x --no-install chex dependency-gates'
|
|
136
|
-
- name: typecheck
|
|
138
|
+
- name: chex-typecheck
|
|
137
139
|
run: '{clear-git-env} bun x --no-install diet -- bun run typecheck'
|
|
138
140
|
# A bunfig.toml reaches the job too, because a change to it skips the
|
|
139
141
|
# related tests.
|
|
140
|
-
- name: test
|
|
142
|
+
- name: chex-test
|
|
141
143
|
files:
|
|
142
144
|
unset $(git rev-parse --local-env-vars); bun
|
|
143
145
|
node_modules/@zgeoff/chex/src/read-branch-files.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zgeoff/chex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Run each shared docs and repo check as one subcommand, with the lefthook pre-push baseline that calls them",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"checks",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/bun": "1.4.4",
|
|
44
|
-
"@zgeoff/rig": "0.
|
|
44
|
+
"@zgeoff/rig": "0.2.0",
|
|
45
45
|
"@zgeoff/std": "0.4.2",
|
|
46
46
|
"@zgeoff/testing": "0.2.1",
|
|
47
47
|
"tiny-invariant": "1.3.3",
|
|
@@ -9,6 +9,8 @@ export function collectStaleAgeGateExcludes(
|
|
|
9
9
|
): DependencyGateFinding[] {
|
|
10
10
|
const removalDates = collectRemovalDates(bunfig);
|
|
11
11
|
|
|
12
|
+
// Renovate bumps a first-party package as soon as it publishes, so its exclude stays for good and
|
|
13
|
+
// carries no removal date.
|
|
12
14
|
return parseAgeGate(bunfig)
|
|
13
15
|
.excludes.filter((name) => !firstPartyScopes.some((scope) => name.startsWith(scope)))
|
|
14
16
|
.flatMap((name) => {
|
package/src/parse-lane-limits.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// The limits match the max-test-timeout rule in @zgeoff/oxlint-config, which
|
|
2
|
-
//
|
|
1
|
+
// The limits match the max-test-timeout rule in @zgeoff/oxlint-config, which neither chex nor the
|
|
2
|
+
// copy that repo-sync puts in each repo can import.
|
|
3
3
|
const standardLaneLimits: readonly (readonly [string, number])[] = [
|
|
4
4
|
['browser', 10_000],
|
|
5
5
|
['e2e', 15_000],
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { findMember } from './find-member.ts';
|
|
2
|
+
|
|
3
|
+
export type ParsedRigLaneLimits =
|
|
4
|
+
| { readonly kind: 'parsed'; readonly limits: ReadonlyMap<string, number> }
|
|
5
|
+
| { readonly kind: 'refused'; readonly reason: string };
|
|
6
|
+
|
|
7
|
+
// chex has no dependency on rig, and the copy that repo-sync puts in each repo imports no package,
|
|
8
|
+
// so this reads the rig config's default export field by field. That export holds every standard
|
|
9
|
+
// lane too, each with its limit in ms.
|
|
10
|
+
export function parseRigLaneLimits(rigConfig: unknown): ParsedRigLaneLimits {
|
|
11
|
+
const lanes = findMember(rigConfig, 'lanes');
|
|
12
|
+
|
|
13
|
+
if (!Array.isArray(lanes)) {
|
|
14
|
+
return { kind: 'refused', reason: 'its default export has no list of lanes' };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const limits = new Map<string, number>();
|
|
18
|
+
|
|
19
|
+
for (const [index, lane] of lanes.entries()) {
|
|
20
|
+
const name = findMember(lane, 'name');
|
|
21
|
+
const limitMs = findMember(lane, 'limitMs');
|
|
22
|
+
|
|
23
|
+
if (typeof name !== 'string' || typeof limitMs !== 'number') {
|
|
24
|
+
return { kind: 'refused', reason: `lane ${index} lacks a name or a limitMs` };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
limits.set(name, limitMs);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { kind: 'parsed', limits };
|
|
31
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { basename, dirname, posix } from 'node:path';
|
|
1
|
+
import { basename, dirname, join, posix } from 'node:path';
|
|
2
|
+
import { findMember } from './find-member.ts';
|
|
2
3
|
import { parseLaneLimits } from './parse-lane-limits.ts';
|
|
4
|
+
import { parseRigLaneLimits } from './parse-rig-lane-limits.ts';
|
|
3
5
|
import type { ShellWord } from './parse-shell-words.ts';
|
|
4
6
|
import { parseShellWords } from './parse-shell-words.ts';
|
|
5
7
|
import type { RunProgram } from './run-program.ts';
|
|
@@ -9,6 +11,7 @@ export interface TestTimeoutsCheckOptions {
|
|
|
9
11
|
readonly cwd: string;
|
|
10
12
|
readonly env: Readonly<Record<string, string | undefined>>;
|
|
11
13
|
readonly runProgram: RunProgram;
|
|
14
|
+
readonly importModule: (absolutePath: string) => Promise<unknown>;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
export interface TestTimeoutFinding {
|
|
@@ -19,16 +22,12 @@ export interface TestTimeoutFinding {
|
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
// The check reads the files as HEAD holds them, so a push checks what it sends.
|
|
22
|
-
export function runTestTimeoutsCheck(
|
|
25
|
+
export async function runTestTimeoutsCheck(
|
|
23
26
|
options: Readonly<TestTimeoutsCheckOptions>,
|
|
24
|
-
): TestTimeoutFinding[] {
|
|
27
|
+
): Promise<TestTimeoutFinding[]> {
|
|
25
28
|
const paths = runGit(options, ['ls-tree', '-r', '-z', '--name-only', 'HEAD']).split('\0');
|
|
26
29
|
|
|
27
|
-
const
|
|
28
|
-
? runGit(options, ['show', `HEAD:${oxlintConfigPath}`])
|
|
29
|
-
: null;
|
|
30
|
-
|
|
31
|
-
const limits = parseLaneLimits(configText);
|
|
30
|
+
const limits = await readLaneLimits(options, paths);
|
|
32
31
|
|
|
33
32
|
const appRoots = new Set(
|
|
34
33
|
paths.filter((path) => basename(path) === 'package.json').map((path) => dirname(path)),
|
|
@@ -54,7 +53,6 @@ export function runTestTimeoutsCheck(
|
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
const configPattern = /^\.?oxlintrc(?:\.[\w-]+)?\.jsonc?$/u;
|
|
57
|
-
const oxlintConfigPath = '.oxlintrc.json';
|
|
58
56
|
|
|
59
57
|
const checkedFiles = new Set(['package.json', 'bunfig.toml']);
|
|
60
58
|
|
|
@@ -68,6 +66,51 @@ function runGit(options: Readonly<TestTimeoutsCheckOptions>, args: readonly stri
|
|
|
68
66
|
return result.stdout;
|
|
69
67
|
}
|
|
70
68
|
|
|
69
|
+
const rigConfigPath = 'rig.config.ts';
|
|
70
|
+
const oxlintConfigPath = '.oxlintrc.json';
|
|
71
|
+
|
|
72
|
+
// oxlint-config refuses the lanes option of its timeout rule in a repo with a rig.config.ts, so
|
|
73
|
+
// that file alone sets the lanes there.
|
|
74
|
+
async function readLaneLimits(
|
|
75
|
+
options: Readonly<TestTimeoutsCheckOptions>,
|
|
76
|
+
paths: readonly string[],
|
|
77
|
+
): Promise<ReadonlyMap<string, number>> {
|
|
78
|
+
if (!paths.includes(rigConfigPath)) {
|
|
79
|
+
const configText = paths.includes(oxlintConfigPath)
|
|
80
|
+
? runGit(options, ['show', `HEAD:${oxlintConfigPath}`])
|
|
81
|
+
: null;
|
|
82
|
+
|
|
83
|
+
return parseLaneLimits(configText);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Only an import resolves the rig config's lanes, and an import reads the working tree, so the
|
|
87
|
+
// check runs only while the working tree holds HEAD's copy.
|
|
88
|
+
const diff = options.runProgram(['git', 'diff', '--quiet', 'HEAD', '--', rigConfigPath], {
|
|
89
|
+
cwd: options.cwd,
|
|
90
|
+
env: options.env,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (diff.exitCode === 1) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`${rigConfigPath} in the working tree differs from HEAD; commit or stash the change, because the check reads the lane limits from HEAD`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (diff.exitCode !== 0) {
|
|
100
|
+
throw new Error(`git diff --quiet HEAD -- ${rigConfigPath} failed: ${diff.stderr.trim()}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const loaded = await options.importModule(join(options.cwd, rigConfigPath));
|
|
104
|
+
|
|
105
|
+
const parsed = parseRigLaneLimits(findMember(loaded, 'default'));
|
|
106
|
+
|
|
107
|
+
if (parsed.kind === 'refused') {
|
|
108
|
+
throw new Error(`cannot read the lanes of ${rigConfigPath}: ${parsed.reason}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return parsed.limits;
|
|
112
|
+
}
|
|
113
|
+
|
|
71
114
|
// A setting that turns a timeout rule off or down to a warning escapes the limits as a disable
|
|
72
115
|
// comment does, so it needs the same ticket, in a comment on its line or the lines above it.
|
|
73
116
|
function collectConfigFindings(path: string, text: string): TestTimeoutFinding[] {
|
|
@@ -288,12 +331,6 @@ function collectScriptFindings(
|
|
|
288
331
|
});
|
|
289
332
|
}
|
|
290
333
|
|
|
291
|
-
function findMember(value: unknown, key: string): unknown {
|
|
292
|
-
return typeof value === 'object' && value !== null && Object.hasOwn(value, key)
|
|
293
|
-
? Reflect.get(value, key)
|
|
294
|
-
: undefined;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
334
|
interface LaneLimit {
|
|
298
335
|
readonly lane: string;
|
|
299
336
|
readonly limitMs: number;
|
|
@@ -475,9 +512,14 @@ const ticketPattern = /\b[A-Z][A-Z0-9]*-\d+\b/u;
|
|
|
475
512
|
const floorPattern = /\bfloor:[\s*]*[^\s*]/u;
|
|
476
513
|
|
|
477
514
|
// Returns the exit code: 0 when clean, 1 with a finding, and 2 when the check cannot run.
|
|
478
|
-
export function runTestTimeoutsCommand(): number {
|
|
515
|
+
export async function runTestTimeoutsCommand(): Promise<number> {
|
|
479
516
|
try {
|
|
480
|
-
const findings = runTestTimeoutsCheck({
|
|
517
|
+
const findings = await runTestTimeoutsCheck({
|
|
518
|
+
cwd: process.cwd(),
|
|
519
|
+
env: process.env,
|
|
520
|
+
runProgram,
|
|
521
|
+
importModule: (path) => import(path),
|
|
522
|
+
});
|
|
481
523
|
|
|
482
524
|
for (const finding of findings) {
|
|
483
525
|
process.stdout.write(`${finding.path}: ${finding.message}\n`);
|
|
@@ -494,5 +536,5 @@ export function runTestTimeoutsCommand(): number {
|
|
|
494
536
|
}
|
|
495
537
|
|
|
496
538
|
if (import.meta.main) {
|
|
497
|
-
process.exitCode = runTestTimeoutsCommand();
|
|
539
|
+
process.exitCode = await runTestTimeoutsCommand();
|
|
498
540
|
}
|