@avi2dg/checks 0.57.0 → 0.57.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/CHANGELOG.md +8 -0
- package/commitlint.config.ts +0 -18
- package/docs/configs/commit-messages.md +1 -1
- package/docs/configs/effect-rules.md +2 -2
- package/docs/gates/checks-docs-only.md +4 -3
- package/docs/gates/checks-graphwalk.md +10 -1
- package/package.json +1 -1
- package/src/complexity/graphwalk.ts +21 -3
- package/src/core/git.ts +4 -1
- package/src/delivery/release-push.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
|
|
4
4
|
|
|
5
|
+
## 0.57.1
|
|
6
|
+
|
|
7
|
+
Released 2026-10-29.
|
|
8
|
+
|
|
9
|
+
### Fixes
|
|
10
|
+
|
|
11
|
+
- **complexity:** time out graphwalk and its dependency cruiser child [#18](https://github.com/avi2d/checks/pull/18)
|
|
12
|
+
|
|
5
13
|
## 0.57.0
|
|
6
14
|
|
|
7
15
|
Released 2026-10-27.
|
package/commitlint.config.ts
CHANGED
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
import configConventional from "@commitlint/config-conventional";
|
|
2
|
-
|
|
3
|
-
const houseTypes: ReadonlyArray<string> = [
|
|
4
|
-
"theme",
|
|
5
|
-
"renamer",
|
|
6
|
-
"shell",
|
|
7
|
-
"pi",
|
|
8
|
-
"home",
|
|
9
|
-
"audit",
|
|
10
|
-
"comments",
|
|
11
|
-
"unslop",
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
const conventionalEnum = configConventional.rules["type-enum"];
|
|
15
|
-
|
|
16
1
|
export default {
|
|
17
2
|
extends: ["@commitlint/config-conventional"],
|
|
18
|
-
rules: {
|
|
19
|
-
"type-enum": [conventionalEnum[0], conventionalEnum[1], [...conventionalEnum[2], ...houseTypes]],
|
|
20
|
-
},
|
|
21
3
|
};
|
|
@@ -8,7 +8,7 @@ The shared commitlint config holds each pull request title to the conventional c
|
|
|
8
8
|
|
|
9
9
|
## Config
|
|
10
10
|
|
|
11
|
-
Commits follow `@commitlint/config-conventional
|
|
11
|
+
Commits follow `@commitlint/config-conventional`, shared from `node_modules/@avi2dg/checks/commitlint.config.ts`.
|
|
12
12
|
It arrives with the kit, since `@commitlint/cli` and `@commitlint/config-conventional` are dependencies, not peers.
|
|
13
13
|
|
|
14
14
|
## Workflow
|
|
@@ -28,8 +28,8 @@ import { defineConfig } from "@avi2dg/checks/oxlint";
|
|
|
28
28
|
export default defineConfig({
|
|
29
29
|
effect: {
|
|
30
30
|
excludeFiles: [
|
|
31
|
-
//
|
|
32
|
-
"
|
|
31
|
+
// The host loads this entry outside the Effect runtime.
|
|
32
|
+
"src/host/**",
|
|
33
33
|
],
|
|
34
34
|
},
|
|
35
35
|
});
|
|
@@ -9,7 +9,7 @@ audience: consumers
|
|
|
9
9
|
## What it checks
|
|
10
10
|
|
|
11
11
|
A change is docs-only when every file it touches is a `.md` file at the top level or under `docs/`, and it touches at least one file.
|
|
12
|
-
Markdown anywhere else, such as a test fixture under `tests/` or a template under `dist/`, keeps the
|
|
12
|
+
Markdown anywhere else, such as a test fixture under `tests/` or a template under `dist/`, keeps the typecheck and unit test steps running.
|
|
13
13
|
A rename counts both sides.
|
|
14
14
|
An empty change and a change with no range keep the full run.
|
|
15
15
|
|
|
@@ -41,10 +41,11 @@ docs-only=true
|
|
|
41
41
|
|
|
42
42
|
## When it runs
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
The `static` and `unit` jobs of the `ci` workflow append the verdict to `GITHUB_OUTPUT` in a filter step.
|
|
45
45
|
Their typecheck and test steps skip when the verdict reads `true`.
|
|
46
|
+
The `e2e` job does not read this verdict: `scripts/e2e-skip.ts` skips it for a change of only Markdown and the root `copy.json`, a wider set than this bin's.
|
|
46
47
|
The build, its `git diff --exit-code` and `bun run lint` still run, so `checks-docs` and the generated blocks hold on a docs-only change.
|
|
47
|
-
|
|
48
|
+
The `checks` job still runs and reports, so its required check stays green on a docs-only change.
|
|
48
49
|
|
|
49
50
|
## Related topics
|
|
50
51
|
|
|
@@ -55,6 +55,14 @@ It cruises against the first config it finds there:
|
|
|
55
55
|
It runs dependency-cruiser from the installed kit under Bun, which loads a TypeScript config wherever it sits.
|
|
56
56
|
It leaves out a tracked file the working tree no longer holds.
|
|
57
57
|
|
|
58
|
+
## Time limits
|
|
59
|
+
|
|
60
|
+
A run that lasts past 300 seconds stops, prints `graphwalk: timed out after 300s; set CHECKS_GRAPHWALK_TIMEOUT_SECONDS to allow longer` and exits 2.
|
|
61
|
+
A dependency-cruiser run that lasts past 120 seconds stops, and the imports section prints `imports: dependency-cruiser timed out after 120s; set CHECKS_DEPCRUISE_TIMEOUT_SECONDS to allow longer` and cannot decide.
|
|
62
|
+
`CHECKS_GRAPHWALK_TIMEOUT_SECONDS` and `CHECKS_DEPCRUISE_TIMEOUT_SECONDS` each take a whole number of seconds above 0, and any other value leaves the run or the section undecided.
|
|
63
|
+
When a limit passes, the gate sends SIGTERM to the process group of each process it started.
|
|
64
|
+
It sends SIGKILL 2 seconds later to any process still in that group, so nothing it started outlives the gate.
|
|
65
|
+
|
|
58
66
|
## Arguments
|
|
59
67
|
|
|
60
68
|
```sh
|
|
@@ -81,9 +89,10 @@ A seed written after the range adds a dead export, or after it removes the last
|
|
|
81
89
|
| --- | --- |
|
|
82
90
|
| 0 | no file is unreferenced, no symbol is unlisted, added or stale, and no import breaks an `error` rule, or `--write` recorded the baseline |
|
|
83
91
|
| 1 | a file is unreferenced, a symbol is unlisted, added or stale, an import breaks an `error` rule, or the repository holds no Knip configuration |
|
|
84
|
-
| 2 | a ref does not resolve, a scanner cannot run, or a Knip report, a cruise report or the baseline does not parse |
|
|
92
|
+
| 2 | a ref does not resolve, a scanner cannot run, the run or dependency-cruiser passes its time limit, or a Knip report, a cruise report or the baseline does not parse |
|
|
85
93
|
|
|
86
94
|
The gate exits 1 when any section reports a violation, and exits 2 only when no section reports one and at least one section cannot decide.
|
|
95
|
+
When the run passes its time limit the gate exits 2 even when a section already reported a violation.
|
|
87
96
|
|
|
88
97
|
It also exits 2 when the repository tracks no `.ts`, `.tsx`, `.astro`, `.mts` or `.cts` file, or, with `--write`, when it tracks no TypeScript file.
|
|
89
98
|
A repository that tracks `.astro` files but no `.ts`, `.tsx`, `.mts` or `.cts` file prints only the unreferenced-files section.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { BunServices } from "@effect/platform-bun";
|
|
3
|
-
import { Console, Effect, FileSystem, Path, Schema } from "effect";
|
|
3
|
+
import { Config, Console, Duration, Effect, FileSystem, Path, Schema } from "effect";
|
|
4
4
|
import { LINTED_SOURCE, TYPESCRIPT_SOURCE, type TrackedContent } from "../core/gates.ts";
|
|
5
5
|
import { checkoutFiles, collect, commitOf, git, pathsAt, rangeEnds, refArgs } from "../core/git.ts";
|
|
6
6
|
import { PASSED, runExitMain, UNDECIDED, Usage, VIOLATED } from "../core/main.ts";
|
|
@@ -34,6 +34,10 @@ export const CRUISED = ["*.ts", "*.tsx", "*.mts", "*.cts"] as const;
|
|
|
34
34
|
|
|
35
35
|
const KIT_DEFAULTS = "kit-defaults.ts";
|
|
36
36
|
|
|
37
|
+
type TimeLimit = { readonly variable: string; readonly seconds: number };
|
|
38
|
+
const RUN_LIMIT = { variable: "CHECKS_GRAPHWALK_TIMEOUT_SECONDS", seconds: 300 } satisfies TimeLimit;
|
|
39
|
+
const CRUISE_LIMIT = { variable: "CHECKS_DEPCRUISE_TIMEOUT_SECONDS", seconds: 120 } satisfies TimeLimit;
|
|
40
|
+
|
|
37
41
|
export type Status = "passed" | "violated" | "undecided";
|
|
38
42
|
|
|
39
43
|
class GraphwalkError extends Schema.TaggedError<GraphwalkError>()("GraphwalkError", {
|
|
@@ -75,6 +79,15 @@ function outside(symbols: readonly UnusedSymbol[], others: readonly UnusedSymbol
|
|
|
75
79
|
return symbols.filter((symbol) => !known.has(keyOf(symbol))).toSorted(byPosition);
|
|
76
80
|
}
|
|
77
81
|
|
|
82
|
+
const withinLimit = Effect.fn("withinLimit")(function* <A, E, R>(prefix: string, { variable, seconds: fallback }: TimeLimit, body: Effect.Effect<A, E, R>) {
|
|
83
|
+
const seconds = yield* Config.schema(Schema.Int.check(Schema.isGreaterThan(0)), variable).pipe(
|
|
84
|
+
Config.withDefault(fallback),
|
|
85
|
+
Effect.mapError(() => new GraphwalkError({ message: `${variable} must be a whole number of seconds above 0` })),
|
|
86
|
+
);
|
|
87
|
+
const message = `${prefix}timed out after ${seconds}s; set ${variable} to allow longer`;
|
|
88
|
+
return yield* body.pipe(Effect.timeoutOrElse({ duration: Duration.seconds(seconds), orElse: () => Effect.fail(new GraphwalkError({ message })) }));
|
|
89
|
+
});
|
|
90
|
+
|
|
78
91
|
const describe = (symbol: UnusedSymbol): string => ` ${symbol.file}: ${symbol.name} (${symbol.kind})`;
|
|
79
92
|
|
|
80
93
|
export const filesOf = Effect.fn("filesOf")(function* (stdout: string) {
|
|
@@ -267,9 +280,10 @@ const importsSection = (root: string) =>
|
|
|
267
280
|
const files = yield* trackedInTree(root);
|
|
268
281
|
if (files.length === 0) return yield* new GraphwalkError({ message: "no tracked TypeScript to cruise" });
|
|
269
282
|
const config = yield* configOf(root);
|
|
270
|
-
const
|
|
283
|
+
const cruise = collect(process.execPath, [yield* depcruise(), "--config", config.file, "--output-type", "json", ...files], root).pipe(
|
|
271
284
|
Effect.mapError((cause) => new GraphwalkError({ message: `cannot run dependency-cruiser: ${cause.message}` })),
|
|
272
285
|
);
|
|
286
|
+
const run = yield* withinLimit("dependency-cruiser ", CRUISE_LIMIT, cruise);
|
|
273
287
|
const { summary } = yield* decodeCruise(run.stdout).pipe(
|
|
274
288
|
Effect.mapError(
|
|
275
289
|
() => new GraphwalkError({ message: `dependency-cruiser exits ${run.exitCode} with no report: ${(run.stdout + run.stderr).trim()}` }),
|
|
@@ -369,6 +383,10 @@ const gate = Effect.gen(function* () {
|
|
|
369
383
|
),
|
|
370
384
|
);
|
|
371
385
|
|
|
386
|
+
const timedGate = withinLimit("", RUN_LIMIT, gate).pipe(
|
|
387
|
+
Effect.catch((cause) => Console.error(`${NAME}: ${cause.message}`).pipe(Effect.as(UNDECIDED))),
|
|
388
|
+
);
|
|
389
|
+
|
|
372
390
|
if (import.meta.main) {
|
|
373
|
-
runExitMain(
|
|
391
|
+
runExitMain(timedGate);
|
|
374
392
|
}
|
package/src/core/git.ts
CHANGED
|
@@ -15,12 +15,15 @@ export type Feed = {
|
|
|
15
15
|
readonly input?: string;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
// Without a SIGKILL deadline, an interrupted child that ignores SIGTERM holds its scope open forever.
|
|
19
|
+
const KILL_GRACE = "2 seconds";
|
|
20
|
+
|
|
18
21
|
// Both pipes drain while the program runs: one left unread fills its buffer and stalls it.
|
|
19
22
|
export const collect = Effect.fn("collect")(
|
|
20
23
|
function* (program: string, args: readonly string[], cwd?: string, { env, input }: Feed = {}) {
|
|
21
24
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
22
25
|
const stdin = input === undefined ? undefined : Stream.make(new TextEncoder().encode(input));
|
|
23
|
-
const handle = yield* spawner.spawn(ChildProcess.make(program, args, { cwd, env, extendEnv: true, stdin }));
|
|
26
|
+
const handle = yield* spawner.spawn(ChildProcess.make(program, args, { cwd, env, extendEnv: true, stdin, forceKillAfter: KILL_GRACE }));
|
|
24
27
|
const [stdout, stderr, exitCode] = yield* Effect.all([text(handle.stdout), text(handle.stderr), handle.exitCode], {
|
|
25
28
|
concurrency: "unbounded",
|
|
26
29
|
});
|
|
@@ -101,7 +101,7 @@ const land = Effect.fn("land")(function* (root: string, base: string, head: stri
|
|
|
101
101
|
Effect.gen(function* () {
|
|
102
102
|
const now = listed.split("\t")[0];
|
|
103
103
|
if (now === release) return "landed";
|
|
104
|
-
if (now === head) return yield*
|
|
104
|
+
if (now === head) return yield* failure;
|
|
105
105
|
yield* git(["fetch", "--quiet", "--no-tags", "origin", `refs/heads/${base}`], root);
|
|
106
106
|
const ancestor = yield* git(["merge-base", "--is-ancestor", release, "FETCH_HEAD"], root).pipe(
|
|
107
107
|
Effect.as(true),
|