@avi2dg/checks 0.54.1 → 0.56.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/CHANGELOG.md +16 -0
- package/docs/bins.md +1 -0
- package/docs/configs/dependency-rules.md +1 -1
- package/docs/design.md +4 -3
- package/docs/gates/checks-advisories.md +7 -4
- package/docs/gates/checks-ci-wiring.md +1 -1
- package/docs/gates/checks-copy.md +1 -1
- package/docs/gates/checks-css-size.md +1 -1
- package/docs/gates/checks-effect-scope.md +1 -1
- package/docs/gates/checks-frontend-syntax.md +1 -1
- package/docs/gates/checks-imports.md +1 -1
- package/docs/gates/checks-lint-coverage.md +1 -1
- package/docs/gates/checks-lint.md +13 -3
- package/docs/gates/checks-no-plain-js.md +1 -1
- package/docs/gates/checks-test-layout.md +1 -1
- package/docs/gates/checks-unused.md +1 -1
- package/package.json +3 -1
- package/src/core/gates.ts +12 -1
- package/src/core/lint.ts +42 -5
- package/src/core/scope.ts +54 -0
- package/src/dependencies/advisories.ts +10 -2
- package/src/dependencies/advisory-rules.ts +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
|
|
4
4
|
|
|
5
|
+
## 0.56.0
|
|
6
|
+
|
|
7
|
+
Released 2026-10-27.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **core:** scope lint gates by changed paths and add desk run [#11](https://github.com/avi2d/checks/pull/11)
|
|
12
|
+
|
|
13
|
+
## 0.55.0
|
|
14
|
+
|
|
15
|
+
Released 2026-10-27.
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
- **dependencies:** defer the advisory scan to the scheduled run [#10](https://github.com/avi2d/checks/pull/10)
|
|
20
|
+
|
|
5
21
|
## 0.54.1
|
|
6
22
|
|
|
7
23
|
Released 2026-10-27.
|
package/docs/bins.md
CHANGED
|
@@ -10,6 +10,7 @@ The kit ships one bin per check, each named `checks-` and called from a package
|
|
|
10
10
|
|
|
11
11
|
`checks-lint` runs each of these gates that applies to the repository, and names every one that fails.
|
|
12
12
|
A gate reads either the working tree or the range `checks-lint` resolves.
|
|
13
|
+
It scopes tree gates to the change unless `--full` forces the full run, and `--desk` runs the fast tree gates.
|
|
13
14
|
The table groups the gates by vector, the part of a repository each one judges.
|
|
14
15
|
|
|
15
16
|
<!-- generated gates: bun run build writes it from KIT_GATES in src/core/gates.ts and scripts/doc-blocks.ts -->
|
|
@@ -61,7 +61,7 @@ A rule under `forbidden` that takes a kit rule's name replaces that rule whole,
|
|
|
61
61
|
|
|
62
62
|
## Running it
|
|
63
63
|
|
|
64
|
-
`checks-lint` runs [checks-imports](../gates/checks-imports.md), which cruises every tracked TypeScript file against `dependency-cruiser.config.ts`, or against the kit's defaults when the repository holds no config.
|
|
64
|
+
`checks-lint` runs [checks-imports](../gates/checks-imports.md) in the full run, which cruises every tracked TypeScript file against `dependency-cruiser.config.ts`, or against the kit's defaults when the repository holds no config.
|
|
65
65
|
|
|
66
66
|
## Related topics
|
|
67
67
|
|
package/docs/design.md
CHANGED
|
@@ -123,10 +123,11 @@ So the three packages move together at one exact version.
|
|
|
123
123
|
`checks-lint` runs each gate in a child process rather than importing it.
|
|
124
124
|
A gate then behaves the same alone or through `checks-lint`, and `lint-coverage.sh` can stay a shell script.
|
|
125
125
|
The gates run one at a time and pass their output straight through, so each report reads whole and in the order of the gate table.
|
|
126
|
-
`checks-lint` picks the gates that apply from the tracked files, and a repository cannot select gates.
|
|
127
|
-
A
|
|
126
|
+
`checks-lint` picks the gates that apply from the tracked files for the full run, and a repository cannot select gates.
|
|
127
|
+
A scoped run selects a subset by changed paths, and the desk run selects the fast tree gates.
|
|
128
|
+
A TypeScript gate applies as soon as the repository tracks TypeScript source, and `checks-lint-coverage` and `checks-unused` also apply on tracked Astro source.
|
|
128
129
|
|
|
129
|
-
dependency-cruiser runs as the `checks-imports` gate rather than as a script each repository adds to `lint`, so
|
|
130
|
+
dependency-cruiser runs as the `checks-imports` gate rather than as a script each repository adds to `lint`, so the full run keeps it.
|
|
130
131
|
A repository with no config of its own is cruised against the kit's defaults, since every rule has one.
|
|
131
132
|
The gate cruises the tracked TypeScript, so a build output or a linked library outside git never reaches the cruise.
|
|
132
133
|
It runs dependency-cruiser under Bun, which loads a TypeScript config wherever it sits, the kit's defaults under `node_modules` among them.
|
|
@@ -14,6 +14,7 @@ It ignores aliases, because two live advisories can list each other as aliases.
|
|
|
14
14
|
An advisory published against a package the base already held shows at both ends, so it fails no range, and `--all` reports it instead.
|
|
15
15
|
Each failure names the package, its version, the advisory id, its severity and its summary.
|
|
16
16
|
A range that leaves `bun.lock` unchanged runs no scan.
|
|
17
|
+
A run `checks-lint` starts defers the scan to the scheduled `--all` run instead, so the desk and the pull request neither fetch the database nor scan.
|
|
17
18
|
It also holds `advisory-acks.json` to the rules in [The acknowledgement file](#the-acknowledgement-file) on every run.
|
|
18
19
|
|
|
19
20
|
## What it reads
|
|
@@ -62,6 +63,7 @@ An entry whose `until` falls more than 30 days after that moment fails the run a
|
|
|
62
63
|
Once that moment passes `until`, the entry fails every run until it is deleted or renewed with a later `until`, whether or not the range touches `bun.lock`.
|
|
63
64
|
Delete it once the package is upgraded.
|
|
64
65
|
When a scan runs, an entry that matches no advisory at the head fails, so the file holds only live entries.
|
|
66
|
+
A deferred run still holds each entry to its day, while an entry that matches nothing waits for the scheduled run.
|
|
65
67
|
A file that does not decode as that list exits 2.
|
|
66
68
|
|
|
67
69
|
## Arguments
|
|
@@ -80,9 +82,9 @@ With `--all` it fails on every advisory in `bun.lock` at `HEAD` that no entry co
|
|
|
80
82
|
|
|
81
83
|
| Code | When |
|
|
82
84
|
| --- | --- |
|
|
83
|
-
| 0 | the range adds no advisory
|
|
84
|
-
| 1 |
|
|
85
|
-
| 2 | a ref does not resolve, `advisory-acks.json` does not decode, or no verified scanner or usable database
|
|
85
|
+
| 0 | the range adds no advisory on a direct scan or an unchanged range and every acknowledgement holds, or a deferred run holds every acknowledgement without a scan |
|
|
86
|
+
| 1 | a direct scan adds an advisory no acknowledgement covers, or an acknowledgement does not hold on any run |
|
|
87
|
+
| 2 | a ref does not resolve, `advisory-acks.json` does not decode, or a direct scan has no verified scanner or usable database at hand |
|
|
86
88
|
|
|
87
89
|
## Sample output
|
|
88
90
|
|
|
@@ -97,7 +99,8 @@ advisories: 1 acknowledgement(s) in advisory-acks.json do not hold:
|
|
|
97
99
|
## When it runs
|
|
98
100
|
|
|
99
101
|
`checks-lint` runs it over each pull request's range in a repository that tracks `bun.lock`, as [checks-lint](checks-lint.md) says.
|
|
100
|
-
|
|
102
|
+
That run defers the scan to the scheduled run, so it fetches no database and reports the deferral instead of a verdict.
|
|
103
|
+
A range that leaves `bun.lock` unchanged runs in under a second, and a direct run over a range that changes `bun.lock` scans for about 10 seconds on a warm cache.
|
|
101
104
|
|
|
102
105
|
## Running it on a schedule
|
|
103
106
|
|
|
@@ -107,7 +107,7 @@ copy: no violation in 2 file(s) from 2 declared input(s)
|
|
|
107
107
|
|
|
108
108
|
## When it runs
|
|
109
109
|
|
|
110
|
-
`checks-lint` runs it when the repository tracks `copy.json` at its root.
|
|
110
|
+
`checks-lint` runs it in the full run when the repository tracks `copy.json` at its root.
|
|
111
111
|
A repository without that file never runs it and needs no html-validate.
|
|
112
112
|
|
|
113
113
|
## Related topics
|
|
@@ -55,7 +55,7 @@ effect-scope: the @effect/language-service entry in tsconfig.json differs from t
|
|
|
55
55
|
## When it runs
|
|
56
56
|
|
|
57
57
|
A repository runs `checks-effect-scope` from its `build` script, so CI's `git diff --exit-code` after the build fails on a stale `tsconfig.json` too.
|
|
58
|
-
`checks-lint` runs it with `--check` when the repository tracks `oxlint.config.ts`.
|
|
58
|
+
`checks-lint` runs it with `--check` in the full run when the repository tracks `oxlint.config.ts`.
|
|
59
59
|
|
|
60
60
|
## Related topics
|
|
61
61
|
|
|
@@ -177,7 +177,7 @@ frontend-syntax: no violation in 2 file(s) from 2 declared input(s)
|
|
|
177
177
|
|
|
178
178
|
## When it runs
|
|
179
179
|
|
|
180
|
-
`checks-lint` runs it when the repository tracks `frontend-syntax.json` at its root.
|
|
180
|
+
`checks-lint` runs it in the full run when the repository tracks `frontend-syntax.json` at its root.
|
|
181
181
|
A repository without that file never runs it and needs neither `stylelint.config.ts` nor the peers.
|
|
182
182
|
|
|
183
183
|
## Related topics
|
|
@@ -56,7 +56,7 @@ imports: 225 module(s) cruised against dependency-cruiser.config.ts, no violatio
|
|
|
56
56
|
|
|
57
57
|
## When it runs
|
|
58
58
|
|
|
59
|
-
`checks-lint` runs it when the repository tracks a `.ts` or `.tsx` file, so a repository with no config of its own still has its imports judged.
|
|
59
|
+
`checks-lint` runs it in the full run when the repository tracks a `.ts` or `.tsx` file, so a repository with no config of its own still has its imports judged, and never in the desk run.
|
|
60
60
|
|
|
61
61
|
## Related topics
|
|
62
62
|
|
|
@@ -53,7 +53,7 @@ lint-coverage: the program tsconfig.json builds holds the ts-reset rules is-arra
|
|
|
53
53
|
|
|
54
54
|
## When it runs
|
|
55
55
|
|
|
56
|
-
`checks-lint` runs it when the repository tracks a `.ts`, `.tsx` or `.astro` file.
|
|
56
|
+
`checks-lint` runs it in the full run when the repository tracks a `.ts`, `.tsx` or `.astro` file.
|
|
57
57
|
|
|
58
58
|
## Related topics
|
|
59
59
|
|
|
@@ -4,14 +4,20 @@ audience: consumers
|
|
|
4
4
|
---
|
|
5
5
|
# checks-lint
|
|
6
6
|
|
|
7
|
-
`checks-lint` runs every applicable kit lint gate over one range and reports each failure.
|
|
7
|
+
`checks-lint` runs every applicable kit lint gate in the selected set over one range and reports each failure.
|
|
8
8
|
|
|
9
9
|
## What it checks
|
|
10
10
|
|
|
11
11
|
It runs the gates under [What runs](../bins.md#what-runs), each in its own process.
|
|
12
|
+
It scopes the run to the gates the change reaches, and the full run stays the fallback.
|
|
13
|
+
Applicability below selects the full run, and a scoped or desk run selects a subset.
|
|
14
|
+
A Markdown change runs the range gates plus `checks-copy`.
|
|
15
|
+
A TypeScript source change runs every applicable gate.
|
|
16
|
+
A test file classifies as TypeScript, so its change runs every applicable gate too.
|
|
12
17
|
Gates requiring tracked TypeScript files begin running when the repository tracks TypeScript.
|
|
13
18
|
`checks-lint-coverage` and `checks-unused` also begin running when the repository tracks an `.astro` file.
|
|
14
19
|
`checks-advisories` begins running when the repository tracks `bun.lock`.
|
|
20
|
+
`checks-lint` defers its scan to the scheduled run, so the desk and the pull request fetch no database.
|
|
15
21
|
`checks-frontend-syntax` begins running when the repository tracks `frontend-syntax.json` at its root.
|
|
16
22
|
`checks-copy` begins running when the repository tracks `copy.json` at its root.
|
|
17
23
|
`checks-css-size` begins running when the repository tracks a `*.css` file.
|
|
@@ -30,16 +36,19 @@ Tree gates read the working tree rather than the range.
|
|
|
30
36
|
|
|
31
37
|
```sh
|
|
32
38
|
checks-lint
|
|
33
|
-
checks-lint
|
|
39
|
+
checks-lint [--full | --desk]
|
|
40
|
+
checks-lint [--full | --desk] <base-ref> <head-ref>
|
|
34
41
|
```
|
|
35
42
|
|
|
36
43
|
With two arguments, the base and head override range discovery.
|
|
44
|
+
`--full` forces the unscoped run.
|
|
45
|
+
`--desk` runs the fast tree gates.
|
|
37
46
|
|
|
38
47
|
## Exit codes
|
|
39
48
|
|
|
40
49
|
| Code | Result |
|
|
41
50
|
| --- | --- |
|
|
42
|
-
| 0 | Every
|
|
51
|
+
| 0 | Every gate that ran passed. |
|
|
43
52
|
| 1 | At least one gate found a violation. |
|
|
44
53
|
| 2 | The range could not resolve or no failed gate could decide. |
|
|
45
54
|
|
|
@@ -59,6 +68,7 @@ checks-lint: 1 of 19 gate(s) failed: checks-comment-gate
|
|
|
59
68
|
A repository runs it from `bun run lint` in a pull request workflow that fetches the whole git history.
|
|
60
69
|
It leaves out the TypeScript gates while the repository tracks no TypeScript file, except that `checks-lint-coverage` and `checks-unused` run when it tracks an `.astro` file.
|
|
61
70
|
It never runs `checks-browser`, which a product runs after it builds its site.
|
|
71
|
+
A pull request run scopes the gates to the change.
|
|
62
72
|
|
|
63
73
|
## Related topics
|
|
64
74
|
|
|
@@ -87,7 +87,7 @@ test-layout: 4 violation(s)
|
|
|
87
87
|
|
|
88
88
|
## When it runs
|
|
89
89
|
|
|
90
|
-
`checks-lint` runs it when the repository tracks TypeScript.
|
|
90
|
+
`checks-lint` runs it in the full run when the repository tracks TypeScript.
|
|
91
91
|
A repository without TypeScript needs neither the `checks-test` script nor `bunfig.toml`.
|
|
92
92
|
|
|
93
93
|
## Related topics
|
|
@@ -57,7 +57,7 @@ unused: no unreferenced files among 110 tracked .ts/.tsx/.astro file(s)
|
|
|
57
57
|
|
|
58
58
|
## When it runs
|
|
59
59
|
|
|
60
|
-
`checks-lint` runs it when the repository tracks a `.ts`, `.tsx` or `.astro` file.
|
|
60
|
+
`checks-lint` runs it in the full run when the repository tracks a `.ts`, `.tsx` or `.astro` file, and never in the desk run.
|
|
61
61
|
Such a repository names its entries in a Knip configuration.
|
|
62
62
|
|
|
63
63
|
## Related topics
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avi2dg/checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.56.0",
|
|
4
4
|
"description": "Deterministic checks shared across a set of TypeScript repositories",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "avi2d",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"src/core/gates.ts",
|
|
35
35
|
"src/core/declared-inputs.ts",
|
|
36
36
|
"src/core/lint.ts",
|
|
37
|
+
"src/core/scope.ts",
|
|
37
38
|
"src/delivery/changelog.ts",
|
|
38
39
|
"src/delivery/changelog-write.ts",
|
|
39
40
|
"src/delivery/release-notes.ts",
|
|
@@ -205,6 +206,7 @@
|
|
|
205
206
|
},
|
|
206
207
|
"scripts": {
|
|
207
208
|
"prepare": "bun src/dependencies/vendor.ts --library effect --package effect --repository https://github.com/Effect-TS/effect.git --tag 'effect@{version}' --path packages/effect/package.json",
|
|
209
|
+
"desk": "bun run test:fast && bun src/core/lint.ts --desk",
|
|
208
210
|
"build": "bun build src/quality/effect-channel/index.ts --outdir dist/effect-channel --target node --format esm && bun build src/complexity/readability/index.ts --outdir dist/readability --target node --format esm && bun build src/quality/data-shape/index.ts --outdir dist/data-shape --target node --format esm && bun build src/testing/mutation-guard-plugin.ts src/testing/mutation-refusal.ts src/testing/unit-test-order.ts --outdir dist/mutation --target node --format esm --packages external && bun build src/quality/presets/oxlint.ts src/quality/presets/knip.ts src/quality/presets/dependency-cruiser.ts src/quality/presets/stylelint.ts --outdir dist/presets --target node --format esm --packages external && tsc -p tsconfig.presets.json && bun scripts/configs-write.ts && bun src/quality/effect-scope.ts && bun scripts/doc-templates-write.ts && bun src/delivery/changelog-write.ts && bun scripts/doc-blocks-write.ts",
|
|
209
211
|
"lint": "oxlint && bun src/core/lint.ts",
|
|
210
212
|
"typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",
|
package/src/core/gates.ts
CHANGED
|
@@ -8,6 +8,9 @@ export type TrackedContent = {
|
|
|
8
8
|
readonly content: string;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
// checks-lint sets this when it spawns the advisories gate, so the desk and pull request runs leave the database fetch and the scan to the scheduled --all run.
|
|
12
|
+
export const ADVISORIES_DEFER_SCAN = "CHECKS_ADVISORIES_DEFER_SCAN";
|
|
13
|
+
|
|
11
14
|
export const EVERY_REPOSITORY = "every repository";
|
|
12
15
|
|
|
13
16
|
export const VECTORS = ["complexity", "quality", "testing", "docs", "delivery", "dependencies"] as const;
|
|
@@ -21,6 +24,7 @@ export type KitGate = {
|
|
|
21
24
|
readonly reads: "tree" | "range";
|
|
22
25
|
readonly alsoReads?: string;
|
|
23
26
|
readonly args?: readonly string[];
|
|
27
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
24
28
|
readonly appliesTo: typeof EVERY_REPOSITORY | TrackedContent;
|
|
25
29
|
};
|
|
26
30
|
|
|
@@ -63,5 +67,12 @@ export const KIT_GATES = [
|
|
|
63
67
|
{ bin: "checks-exports", vector: "complexity", file: "exports.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
|
|
64
68
|
{ bin: "checks-quarantine-clock", vector: "testing", file: "quarantine-clock.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
65
69
|
{ bin: "checks-imports", vector: "dependencies", file: "imports.ts", reads: "tree", appliesTo: TYPESCRIPT_SOURCE },
|
|
66
|
-
{
|
|
70
|
+
{
|
|
71
|
+
bin: "checks-advisories",
|
|
72
|
+
vector: "dependencies",
|
|
73
|
+
file: "advisories.ts",
|
|
74
|
+
reads: "range",
|
|
75
|
+
env: { [ADVISORIES_DEFER_SCAN]: "1" },
|
|
76
|
+
appliesTo: BUN_LOCKFILE,
|
|
77
|
+
},
|
|
67
78
|
] as const satisfies readonly KitGate[];
|
package/src/core/lint.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import { Config, Console, Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
3
3
|
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
4
4
|
import { EVERY_REPOSITORY, KIT_GATES, type KitGate } from "./gates.ts";
|
|
5
|
-
import { defaultBranch, git } from "./git.ts";
|
|
5
|
+
import { changedPaths, defaultBranch, git } from "./git.ts";
|
|
6
6
|
import { runMain, Usage } from "./main.ts";
|
|
7
|
+
import { FAST_TREE_GATES, selectScope } from "./scope.ts";
|
|
7
8
|
|
|
8
9
|
type Range = {
|
|
9
10
|
readonly refs: readonly [tip: string] | readonly [base: string, head: string];
|
|
@@ -21,7 +22,7 @@ class GatesUndecided extends Schema.TaggedError<GatesUndecided>()("GatesUndecide
|
|
|
21
22
|
}) {}
|
|
22
23
|
|
|
23
24
|
const NAME = "checks-lint";
|
|
24
|
-
const USAGE = "usage: lint.ts [<base-ref> <head-ref>]";
|
|
25
|
+
const USAGE = "usage: lint.ts [--full | --desk] [<base-ref> <head-ref>]";
|
|
25
26
|
const PULL_REQUEST_EVENT = "pull_request";
|
|
26
27
|
const SHALLOW_HINT = "a CI checkout needs actions/checkout fetch-depth: 0";
|
|
27
28
|
|
|
@@ -140,6 +141,15 @@ const resolveRange = Effect.fn("resolveRange")(function* (args: readonly string[
|
|
|
140
141
|
return { refs: rangeOf(base, head), source: ends.source } satisfies Range;
|
|
141
142
|
});
|
|
142
143
|
|
|
144
|
+
export type LintMode = "full" | "desk" | "scoped";
|
|
145
|
+
|
|
146
|
+
export function splitMode(args: readonly string[]): { readonly mode: LintMode; readonly rest: readonly string[] } | undefined {
|
|
147
|
+
const flags = args.filter((arg) => arg.startsWith("--"));
|
|
148
|
+
const rest = args.filter((arg) => !arg.startsWith("--"));
|
|
149
|
+
if (flags.length > 1 || (flags.length === 1 && flags[0] !== "--full" && flags[0] !== "--desk")) return undefined;
|
|
150
|
+
return { mode: flags.length === 0 ? "scoped" : flags[0] === "--full" ? "full" : "desk", rest };
|
|
151
|
+
}
|
|
152
|
+
|
|
143
153
|
export function describe({ refs }: Range): string {
|
|
144
154
|
return refs.length === 1 ? `tip ${refs[0]}` : `range ${refs[0]}..${refs[1]}`;
|
|
145
155
|
}
|
|
@@ -154,8 +164,9 @@ const runGate = Effect.fn("runGate")(function* (gate: KitGate, range: Range) {
|
|
|
154
164
|
const script = (yield* Path.Path).join(import.meta.dir, "..", gate.vector, gate.file);
|
|
155
165
|
const program = gate.file.endsWith(".sh") ? "sh" : process.execPath;
|
|
156
166
|
const args = gate.reads === "range" ? [script, ...range.refs] : [script, ...(gate.args ?? [])];
|
|
167
|
+
const extra = gate.env === undefined ? {} : { env: gate.env, extendEnv: true };
|
|
157
168
|
const exitCode = yield* spawner
|
|
158
|
-
.exitCode(ChildProcess.make(program, args, { stdin: "ignore", stdout: "inherit", stderr: "inherit" }))
|
|
169
|
+
.exitCode(ChildProcess.make(program, args, { stdin: "ignore", stdout: "inherit", stderr: "inherit", ...extra }))
|
|
159
170
|
.pipe(
|
|
160
171
|
Effect.catch((cause) =>
|
|
161
172
|
Console.error(`${NAME}: cannot run ${gate.bin}: ${cause.message}`).pipe(Effect.as(ChildProcessSpawner.ExitCode(2))),
|
|
@@ -164,15 +175,41 @@ const runGate = Effect.fn("runGate")(function* (gate: KitGate, range: Range) {
|
|
|
164
175
|
return { gate, outcome: outcomeOf(exitCode) };
|
|
165
176
|
});
|
|
166
177
|
|
|
178
|
+
const scopedGates = Effect.fn("scopedGates")(function* (range: Range, root: string) {
|
|
179
|
+
if (range.refs.length === 1) return { bins: undefined, detail: "the full run, since a lone tip scopes nothing" };
|
|
180
|
+
const [base, head] = range.refs;
|
|
181
|
+
const changes = yield* changedPaths(base, head, [], root);
|
|
182
|
+
const committed = changes.flatMap((change) => (change.kind === "renamed" ? [change.from, change.path] : [change.path]));
|
|
183
|
+
const working = (yield* git(["diff", "--no-renames", "--name-only", "-z", "HEAD", "--"], root)).split("\0").filter(Boolean);
|
|
184
|
+
const untracked = (yield* git(["ls-files", "--others", "--exclude-standard", "-z"], root)).split("\0").filter(Boolean);
|
|
185
|
+
const scope = selectScope([...committed, ...working, ...untracked]);
|
|
186
|
+
if (scope.full) return { bins: undefined, detail: "the full run, since the change reaches past a scoped shape" };
|
|
187
|
+
return { bins: scope.gates, detail: "" };
|
|
188
|
+
});
|
|
189
|
+
|
|
167
190
|
const lint = Effect.gen(function* () {
|
|
191
|
+
const split = splitMode(process.argv.slice(2));
|
|
192
|
+
if (split === undefined) return yield* new Usage({ message: USAGE });
|
|
168
193
|
const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
|
|
169
|
-
const range = yield* resolveRange(
|
|
194
|
+
const range = yield* resolveRange(split.rest);
|
|
170
195
|
yield* Console.log(`${NAME}: ${describe(range)} from ${range.source}`);
|
|
171
|
-
const
|
|
196
|
+
const applicable = yield* Effect.filter(KIT_GATES, (gate) =>
|
|
172
197
|
gate.appliesTo === EVERY_REPOSITORY
|
|
173
198
|
? Effect.succeed(true)
|
|
174
199
|
: git(["ls-files", "--", ...gate.appliesTo.pathspecs], root).pipe(Effect.map((files) => files.trim() !== "")),
|
|
175
200
|
);
|
|
201
|
+
const scoped = split.mode === "desk"
|
|
202
|
+
? {
|
|
203
|
+
bins: applicable.filter((gate) => FAST_TREE_GATES.includes(gate.bin)).map((gate) => gate.bin),
|
|
204
|
+
detail: "the desk run",
|
|
205
|
+
}
|
|
206
|
+
: split.mode === "full"
|
|
207
|
+
? { bins: applicable.map((gate) => gate.bin), detail: "the full run (--full)" }
|
|
208
|
+
: yield* scopedGates(range, root);
|
|
209
|
+
const bins = scoped.bins;
|
|
210
|
+
const gates = bins === undefined ? applicable : applicable.filter((gate) => bins.includes(gate.bin));
|
|
211
|
+
const detail = split.mode === "scoped" && bins !== undefined ? `${gates.length} gate(s) for this change` : scoped.detail;
|
|
212
|
+
yield* Console.log(`${NAME}: ${detail}`);
|
|
176
213
|
const verdicts = yield* Effect.forEach(gates, (gate) => runGate(gate, range));
|
|
177
214
|
const failed = verdicts.filter((verdict) => verdict.outcome !== "passed");
|
|
178
215
|
if (failed.length === 0) {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { KIT_GATES } from "./gates.ts";
|
|
2
|
+
|
|
3
|
+
export type Scope = {
|
|
4
|
+
readonly full: boolean;
|
|
5
|
+
readonly gates: readonly string[];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type ChangeShape = "docs" | "copy" | "typescript" | "workflow" | "lockfile" | "unknown";
|
|
9
|
+
|
|
10
|
+
const TREE_GATES = KIT_GATES.filter((gate) => gate.reads === "tree").map((gate) => gate.bin);
|
|
11
|
+
|
|
12
|
+
// checks-unused and checks-imports each take over a second, so the desk run skips them.
|
|
13
|
+
const SLOW_TREE_GATES: readonly string[] = ["checks-unused", "checks-imports"];
|
|
14
|
+
|
|
15
|
+
export const FAST_TREE_GATES: readonly string[] = TREE_GATES.filter((bin) => !SLOW_TREE_GATES.includes(bin));
|
|
16
|
+
|
|
17
|
+
const CI_CONFIGS = new Set([
|
|
18
|
+
"package.json",
|
|
19
|
+
"bunfig.toml",
|
|
20
|
+
"commitlint.config.ts",
|
|
21
|
+
"oxlint.config.ts",
|
|
22
|
+
"knip.config.ts",
|
|
23
|
+
"knip-base.json",
|
|
24
|
+
"dependency-cruiser.config.js",
|
|
25
|
+
"dependency-cruiser.config.ts",
|
|
26
|
+
]);
|
|
27
|
+
const basenameOf = (path: string): string => path.slice(path.lastIndexOf("/") + 1);
|
|
28
|
+
|
|
29
|
+
export function classifyPath(path: string): ChangeShape {
|
|
30
|
+
const basename = basenameOf(path);
|
|
31
|
+
if (basename === "bun.lock") return "lockfile";
|
|
32
|
+
if (path.startsWith(".github/") || CI_CONFIGS.has(basename) || /^tsconfig(\..*)?\.json$/.test(basename)) {
|
|
33
|
+
return "workflow";
|
|
34
|
+
}
|
|
35
|
+
if (path === "copy.json") return "copy";
|
|
36
|
+
if (/\.(?:ts|tsx|mts|cts|astro)$/.test(path)) return "typescript";
|
|
37
|
+
if (path.endsWith(".md")) return "docs";
|
|
38
|
+
return "unknown";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const fullScope = (): Scope => ({ full: true, gates: KIT_GATES.map((gate) => gate.bin) });
|
|
42
|
+
|
|
43
|
+
export function selectScope(paths: readonly string[]): Scope {
|
|
44
|
+
if (paths.length === 0) return fullScope();
|
|
45
|
+
const shapes = new Set(paths.map(classifyPath));
|
|
46
|
+
if (shapes.has("unknown") || shapes.has("workflow") || shapes.has("lockfile")) return fullScope();
|
|
47
|
+
const copyContent = shapes.has("copy") || shapes.has("docs");
|
|
48
|
+
const tree = shapes.has("typescript") ? TREE_GATES : copyContent ? ["checks-copy"] : [];
|
|
49
|
+
return {
|
|
50
|
+
full: false,
|
|
51
|
+
gates: KIT_GATES.filter((gate) => gate.reads === "range" || tree.includes(gate.bin)).map((gate) => gate.bin),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Clock, Config, Console, Effect, FileSystem, type Layer, Option, Path, Schema } from "effect";
|
|
3
|
+
import { ADVISORIES_DEFER_SCAN } from "../core/gates.ts";
|
|
3
4
|
import { changedPaths, commitOf, git, pathsAt, rangeFromArgs, writtenAt } from "../core/git.ts";
|
|
4
5
|
import type { BunServices } from "@effect/platform-bun";
|
|
5
6
|
import { runMain, Usage } from "../core/main.ts";
|
|
@@ -80,10 +81,17 @@ const findingsOf = Effect.fn("findingsOf")(function* (lockfiles: Lockfiles) {
|
|
|
80
81
|
return { base: findingsIn(scanned, at("base")), head: findingsIn(scanned, at("head")) };
|
|
81
82
|
}, Effect.scoped);
|
|
82
83
|
|
|
83
|
-
const
|
|
84
|
+
const defersScan = Effect.fn("defersScan")(function* () {
|
|
85
|
+
const value = yield* Config.option(Config.String(ADVISORIES_DEFER_SCAN));
|
|
86
|
+
return Option.isSome(value) && value.value.trim() !== "";
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const outcomeOf = Effect.fn("outcomeOf")(function* (request: Request, root: string, clock: AcknowledgementClock, deferred: boolean) {
|
|
84
90
|
if (request.kind === "range" && (yield* changedPaths(request.base, request.head, [LOCKFILE], root)).length === 0) {
|
|
85
91
|
return { kind: "unchanged", problems: clock.problems } satisfies Outcome;
|
|
86
92
|
}
|
|
93
|
+
// The lint path leaves the verdict to the scheduled run, so a changed lockfile reports the deferral instead of scanning.
|
|
94
|
+
if (deferred && request.kind === "range") return { kind: "deferred", problems: clock.problems } satisfies Outcome;
|
|
87
95
|
const head = yield* fileAt(request.head, LOCKFILE, root);
|
|
88
96
|
const base = request.kind === "range" ? yield* fileAt(request.base, LOCKFILE, root) : Option.none<string>();
|
|
89
97
|
const findings = Option.isNone(head) ? { base: [], head: [] } : yield* findingsOf({ base, head });
|
|
@@ -101,7 +109,7 @@ const advisories = Effect.gen(function* () {
|
|
|
101
109
|
const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
|
|
102
110
|
const dates = { head: (yield* writtenAt(request.head, root)) * 1000, now: yield* Clock.currentTimeMillis };
|
|
103
111
|
const clock = clockOf(yield* acknowledgementsAt(request.head, root), { kind: request.kind }, dates);
|
|
104
|
-
const outcome = yield* outcomeOf(request, root, clock);
|
|
112
|
+
const outcome = yield* outcomeOf(request, root, clock, yield* defersScan());
|
|
105
113
|
const text = report(outcome);
|
|
106
114
|
yield* Console.log(text);
|
|
107
115
|
if (request.kind === "head") yield* toStepSummary(text);
|
|
@@ -151,10 +151,11 @@ export function judge(base: readonly Finding[], head: readonly Finding[], clock:
|
|
|
151
151
|
|
|
152
152
|
export type Outcome =
|
|
153
153
|
| { readonly kind: "unchanged"; readonly problems: readonly AcknowledgementProblem[] }
|
|
154
|
+
| { readonly kind: "deferred"; readonly problems: readonly AcknowledgementProblem[] }
|
|
154
155
|
| { readonly kind: "scanned"; readonly scope: Scope; readonly judged: Judged };
|
|
155
156
|
|
|
156
157
|
export function passes(outcome: Outcome): boolean {
|
|
157
|
-
if (outcome.kind === "unchanged") return outcome.problems.length === 0;
|
|
158
|
+
if (outcome.kind === "unchanged" || outcome.kind === "deferred") return outcome.problems.length === 0;
|
|
158
159
|
return outcome.judged.failing.length === 0 && outcome.judged.problems.length === 0;
|
|
159
160
|
}
|
|
160
161
|
|
|
@@ -194,6 +195,12 @@ export function report(outcome: Outcome): string {
|
|
|
194
195
|
if (outcome.kind === "unchanged") {
|
|
195
196
|
return [`advisories: ${LOCKFILE} is unchanged in the range, so nothing was scanned`, ...problemLines(outcome.problems)].join("\n");
|
|
196
197
|
}
|
|
198
|
+
if (outcome.kind === "deferred") {
|
|
199
|
+
return [
|
|
200
|
+
`advisories: ${LOCKFILE} changed in the range, so nothing was scanned; the scheduled --all run owns the verdict`,
|
|
201
|
+
...problemLines(outcome.problems),
|
|
202
|
+
].join("\n");
|
|
203
|
+
}
|
|
197
204
|
return [...scannedLines(outcome.scope, outcome.judged), ...problemLines(outcome.judged.problems)].join("\n");
|
|
198
205
|
}
|
|
199
206
|
|