@avi2dg/checks 0.36.0 → 0.38.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/README.md +33 -20
- package/dependency-cruiser.config.js +92 -60
- package/dist/presets/dependency-cruiser.d.ts +15 -0
- package/dist/presets/dependency-cruiser.js +126 -0
- package/dist/presets/knip.d.ts +6 -0
- package/dist/presets/knip.js +15 -0
- package/dist/presets/oxlint.d.ts +33 -0
- package/dist/presets/oxlint.js +145 -0
- package/docs/configs/dependency-rules.md +28 -31
- package/docs/configs/effect-rules.md +47 -23
- package/docs/configs/native-settings.md +65 -8
- package/docs/configs/typescript-rules.md +7 -7
- package/docs/design.md +25 -8
- package/docs/gates/checks-effect-scope.md +63 -0
- package/docs/gates/checks-exports.md +5 -5
- package/docs/gates/checks-imports.md +64 -0
- package/docs/gates/checks-lint.md +1 -1
- package/docs/gates/checks-mutation-baseline.md +99 -0
- package/docs/gates/checks-mutation-compare.md +5 -2
- package/docs/gates/checks-mutation.md +2 -2
- package/docs/gates/checks-unused.md +4 -4
- package/oxlintrc.json +49 -10
- package/package.json +29 -7
- package/src/complexity/exports.ts +1 -1
- package/src/complexity/knip.ts +1 -1
- package/src/core/gates.ts +4 -0
- package/src/dependencies/imports.ts +94 -0
- package/src/dependencies/kit-defaults.ts +3 -0
- package/src/quality/effect-scope.ts +116 -0
- package/src/quality/jsonc-patch.ts +209 -0
- package/src/quality/presets/dependency-cruiser.ts +148 -0
- package/src/quality/presets/oxlint.ts +165 -0
- package/src/testing/mutation-baseline.ts +211 -0
- package/src/quality/presets/effect.oxlint.json +0 -11
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Config, Console, Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
3
|
+
import { ChildProcessSpawner } from "effect/process";
|
|
4
|
+
import { collect } from "../core/git.ts";
|
|
5
|
+
import { runMain, Usage } from "../core/main.ts";
|
|
6
|
+
import { cacheRoot } from "../dependencies/cache-root.ts";
|
|
7
|
+
|
|
8
|
+
class BaselineFailure extends Schema.TaggedError<BaselineFailure>()("BaselineFailure", {
|
|
9
|
+
message: Schema.String,
|
|
10
|
+
}) {}
|
|
11
|
+
|
|
12
|
+
const NAME = "checks-mutation-baseline";
|
|
13
|
+
const USAGE = "usage: checks-mutation-baseline <incremental-dest> [mutation-json-dest] | --full <mutation-json-dest>";
|
|
14
|
+
const WORKFLOW = "mutation.yml";
|
|
15
|
+
const BRANCH = "main";
|
|
16
|
+
const INCREMENTAL = "stryker-incremental.json";
|
|
17
|
+
const REPORT = "mutation/mutation.json";
|
|
18
|
+
const REPORT_ALONE = "mutation.json";
|
|
19
|
+
const KEPT_PER_ARTIFACT = 2;
|
|
20
|
+
const LATEST_ARTIFACT = "mutation-baseline";
|
|
21
|
+
const FULL_ARTIFACT = "mutation-baseline-full";
|
|
22
|
+
const LATEST_LIMIT = 20;
|
|
23
|
+
const FULL_LIMIT = 50;
|
|
24
|
+
const FULL_EVENTS = ["schedule", "workflow_dispatch"];
|
|
25
|
+
|
|
26
|
+
// The first of `from` the artifact holds is copied to `to`.
|
|
27
|
+
type Copy = { readonly from: readonly string[]; readonly to: string };
|
|
28
|
+
|
|
29
|
+
const Run = Schema.Struct({ databaseId: Schema.Int, event: Schema.String, createdAt: Schema.String });
|
|
30
|
+
type Run = typeof Run.Type;
|
|
31
|
+
|
|
32
|
+
const Artifact = Schema.Struct({
|
|
33
|
+
id: Schema.Int,
|
|
34
|
+
name: Schema.String,
|
|
35
|
+
expired: Schema.Boolean,
|
|
36
|
+
workflow_run: Schema.Struct({ repository_id: Schema.Int }),
|
|
37
|
+
});
|
|
38
|
+
type Artifact = typeof Artifact.Type;
|
|
39
|
+
|
|
40
|
+
const decodeRuns = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Array(Run)));
|
|
41
|
+
const decodeArtifacts = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({ artifacts: Schema.Array(Artifact) })));
|
|
42
|
+
|
|
43
|
+
const gh = Effect.fn("gh")(function* (args: readonly string[]) {
|
|
44
|
+
const failed = (reason: string): BaselineFailure => new BaselineFailure({ message: `gh ${args.join(" ")}: ${reason.trim()}` });
|
|
45
|
+
const { stdout, stderr, exitCode } = yield* collect("gh", args).pipe(Effect.mapError((cause) => failed(cause.message)));
|
|
46
|
+
if (exitCode !== ChildProcessSpawner.ExitCode(0)) return yield* failed(stderr);
|
|
47
|
+
return stdout;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const warned = <A>(fallback: A) => (failure: BaselineFailure) => Console.error(`mutation-baseline: ${failure.message}`).pipe(Effect.as(fallback));
|
|
51
|
+
|
|
52
|
+
const listed = Effect.fn("listed")(function* (event: string | undefined, limit: number) {
|
|
53
|
+
const filter = event === undefined ? [] : ["--event", event];
|
|
54
|
+
const args = ["run", "list", "--workflow", WORKFLOW, "--branch", BRANCH, "--status", "success", ...filter, "--limit", String(limit), "--json", "databaseId,event,createdAt"];
|
|
55
|
+
return yield* gh(args).pipe(
|
|
56
|
+
Effect.flatMap(decodeRuns),
|
|
57
|
+
Effect.mapError((cause) => new BaselineFailure({ message: cause.message })),
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export function newestFirst(runs: readonly Run[]): readonly number[] {
|
|
62
|
+
return [...runs].sort((one, other) => other.createdAt.localeCompare(one.createdAt)).map((run) => run.databaseId);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const latestCandidates = Effect.gen(function* () {
|
|
66
|
+
const newest = (yield* listed(undefined, LATEST_LIMIT)).find((run) => run.event !== "pull_request");
|
|
67
|
+
return newest === undefined ? [] : [newest.databaseId];
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Each full event is listed on its own, because one shared window lets push runs crowd out every full run.
|
|
71
|
+
const fullCandidates = Effect.gen(function* () {
|
|
72
|
+
const perEvent = yield* Effect.forEach(FULL_EVENTS, (event) => listed(event, FULL_LIMIT));
|
|
73
|
+
return newestFirst(perEvent.flat());
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const artifactOf = Effect.fn("artifactOf")(function* (run: number, name: string) {
|
|
77
|
+
const found = yield* gh(["api", `repos/{owner}/{repo}/actions/runs/${run}/artifacts?name=${name}`]).pipe(
|
|
78
|
+
Effect.flatMap(decodeArtifacts),
|
|
79
|
+
Effect.mapError((cause) => new BaselineFailure({ message: cause.message })),
|
|
80
|
+
);
|
|
81
|
+
return Option.fromNullishOr(found.artifacts.find((artifact) => artifact.name === name && !artifact.expired));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const held = Effect.fn("held")(function* (dir: string, names: readonly string[]) {
|
|
85
|
+
const fs = yield* FileSystem.FileSystem;
|
|
86
|
+
const path = yield* Path.Path;
|
|
87
|
+
for (const name of names) {
|
|
88
|
+
const file = path.join(dir, name);
|
|
89
|
+
if (yield* fs.exists(file)) return Option.some(file);
|
|
90
|
+
}
|
|
91
|
+
return Option.none<string>();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const holds = (dir: string, needed: Copy) => held(dir, needed.from).pipe(Effect.map(Option.isSome));
|
|
95
|
+
|
|
96
|
+
const downloaded = Effect.fn("downloaded")(function* (run: number, artifact: Artifact, dir: string, needed: Copy) {
|
|
97
|
+
const fetched = yield* gh(["run", "download", String(run), "--name", artifact.name, "--dir", dir]).pipe(Effect.as(true), Effect.catch(warned(false)));
|
|
98
|
+
return fetched && (yield* holds(dir, needed));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export function pruned(entries: readonly string[], kept: number, restoring: string): readonly string[] {
|
|
102
|
+
return entries
|
|
103
|
+
.filter((entry) => /^\d+$/.test(entry))
|
|
104
|
+
.sort((one, other) => Number(other) - Number(one))
|
|
105
|
+
.slice(kept)
|
|
106
|
+
.filter((entry) => entry !== restoring);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const prune = Effect.fn("prune")(function* (shelf: string, restoring: string) {
|
|
110
|
+
const fs = yield* FileSystem.FileSystem;
|
|
111
|
+
const path = yield* Path.Path;
|
|
112
|
+
for (const entry of pruned(yield* fs.readDirectory(shelf), KEPT_PER_ARTIFACT, restoring)) {
|
|
113
|
+
yield* fs.remove(path.join(shelf, entry), { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
type Fetched = { readonly dir: string; readonly how: string };
|
|
118
|
+
|
|
119
|
+
// The entry lands through a rename beside it, so a job sharing the runner sees it whole or not at all.
|
|
120
|
+
const throughCache = Effect.fn("throughCache")(function* (run: number, artifact: Artifact, root: string, needed: Copy) {
|
|
121
|
+
const fs = yield* FileSystem.FileSystem;
|
|
122
|
+
const path = yield* Path.Path;
|
|
123
|
+
const shelf = path.join(root, "mutation-baseline", String(artifact.workflow_run.repository_id), artifact.name);
|
|
124
|
+
const entry = path.join(shelf, String(artifact.id));
|
|
125
|
+
if (yield* holds(entry, needed)) return Option.some<Fetched>({ dir: entry, how: "restored it from the runner's cache" });
|
|
126
|
+
yield* fs.makeDirectory(shelf, { recursive: true });
|
|
127
|
+
const staged = path.join(yield* fs.makeTempDirectoryScoped({ directory: shelf, prefix: ".staging-" }), "artifact");
|
|
128
|
+
if (!(yield* downloaded(run, artifact, staged, needed))) return Option.none<Fetched>();
|
|
129
|
+
// An entry already there stays, since a job sharing the runner may be copying from it, and the staged copy is whole.
|
|
130
|
+
const placed = yield* fs.rename(staged, entry).pipe(
|
|
131
|
+
Effect.as(entry),
|
|
132
|
+
Effect.orElseSucceed(() => staged),
|
|
133
|
+
);
|
|
134
|
+
yield* prune(shelf, String(artifact.id));
|
|
135
|
+
return Option.some<Fetched>({ dir: placed, how: "downloaded it into the runner's cache" });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const uncached = Effect.fn("uncached")(function* (run: number, artifact: Artifact, needed: Copy) {
|
|
139
|
+
const dir = yield* (yield* FileSystem.FileSystem).makeTempDirectoryScoped({ prefix: "mutation-baseline-" });
|
|
140
|
+
return (yield* downloaded(run, artifact, dir, needed)) ? Option.some<Fetched>({ dir, how: "downloaded it" }) : Option.none<Fetched>();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const restore = Effect.fn("restore")(function* (from: string, needed: Copy, optional: readonly Copy[]) {
|
|
144
|
+
const fs = yield* FileSystem.FileSystem;
|
|
145
|
+
const path = yield* Path.Path;
|
|
146
|
+
const copy = Effect.fn("copy")(function* (file: string, to: string) {
|
|
147
|
+
yield* fs.makeDirectory(path.dirname(to), { recursive: true });
|
|
148
|
+
yield* fs.copyFile(file, to);
|
|
149
|
+
});
|
|
150
|
+
const source = yield* held(from, needed.from);
|
|
151
|
+
if (Option.isNone(source)) return yield* new BaselineFailure({ message: `${from} lost ${needed.from.join(" and ")} before it was copied` });
|
|
152
|
+
yield* copy(source.value, needed.to);
|
|
153
|
+
for (const wanted of optional) {
|
|
154
|
+
const file = yield* held(from, wanted.from);
|
|
155
|
+
if (Option.isSome(file)) yield* copy(file.value, wanted.to);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const restoredFrom = Effect.fn("restoredFrom")(
|
|
160
|
+
function* (run: number, artifact: Artifact, root: Option.Option<string>, request: Request) {
|
|
161
|
+
const fetched = Option.isSome(root) ? yield* throughCache(run, artifact, root.value, request.needed) : yield* uncached(run, artifact, request.needed);
|
|
162
|
+
if (Option.isNone(fetched)) return false;
|
|
163
|
+
yield* restore(fetched.value.dir, request.needed, request.optional);
|
|
164
|
+
yield* Console.log(`mutation-baseline: ${fetched.value.how}, ${artifact.name} artifact ${artifact.id} from run ${run}`);
|
|
165
|
+
return true;
|
|
166
|
+
},
|
|
167
|
+
Effect.scoped,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// A hosted runner is a fresh machine every job, so a cache there only costs a copy.
|
|
171
|
+
const runnerCache = Effect.gen(function* () {
|
|
172
|
+
const environment = yield* Config.String("RUNNER_ENVIRONMENT").pipe(Config.withDefault(""));
|
|
173
|
+
if (environment === "github-hosted") return Option.none<string>();
|
|
174
|
+
return yield* cacheRoot().pipe(
|
|
175
|
+
Effect.asSome,
|
|
176
|
+
Effect.catchTag("CacheUnrooted", () =>
|
|
177
|
+
Console.error("mutation-baseline: the runner cache is disabled because HOME is unset, so the baseline is downloaded").pipe(Effect.as(Option.none<string>())),
|
|
178
|
+
),
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// An artifact that lacks `needed` counts as no baseline, and `optional` copies only what the artifact holds.
|
|
183
|
+
type Request = { readonly full: boolean; readonly needed: Copy; readonly optional: readonly Copy[] };
|
|
184
|
+
|
|
185
|
+
const isDestination = (arg: string | undefined): arg is string => arg !== undefined && !arg.startsWith("-");
|
|
186
|
+
|
|
187
|
+
function parsed(args: readonly string[]): Request | undefined {
|
|
188
|
+
if (args[0] === "--full") {
|
|
189
|
+
const [report, ...extra] = args.slice(1);
|
|
190
|
+
if (!isDestination(report) || extra.length > 0) return undefined;
|
|
191
|
+
return { full: true, needed: { from: [REPORT_ALONE, REPORT], to: report }, optional: [] };
|
|
192
|
+
}
|
|
193
|
+
const [incremental, report, ...extra] = args;
|
|
194
|
+
if (!isDestination(incremental) || extra.length > 0) return undefined;
|
|
195
|
+
return { full: false, needed: { from: [INCREMENTAL], to: incremental }, optional: report === undefined ? [] : [{ from: [REPORT], to: report }] };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const mutationBaseline = Effect.gen(function* () {
|
|
199
|
+
const request = parsed(process.argv.slice(2));
|
|
200
|
+
if (request === undefined) return yield* new Usage({ message: USAGE });
|
|
201
|
+
const name = request.full ? FULL_ARTIFACT : LATEST_ARTIFACT;
|
|
202
|
+
const root = yield* runnerCache;
|
|
203
|
+
for (const run of yield* request.full ? fullCandidates : latestCandidates) {
|
|
204
|
+
const artifact = yield* artifactOf(run, name).pipe(Effect.catch(warned(Option.none<Artifact>())));
|
|
205
|
+
if (Option.isSome(artifact) && (yield* restoredFrom(run, artifact.value, root, request))) return true;
|
|
206
|
+
}
|
|
207
|
+
yield* Console.log(`mutation-baseline: no successful ${BRANCH} run holds a ${name} artifact, so nothing was restored`);
|
|
208
|
+
return true;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (import.meta.main) runMain(NAME, mutationBaseline);
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"plugins": ["node", "promise", "unicorn"],
|
|
3
|
-
"rules": {
|
|
4
|
-
"node/no-sync": "error",
|
|
5
|
-
"oxc/no-async-await": "error",
|
|
6
|
-
"promise/avoid-new": "error",
|
|
7
|
-
"unicorn/no-process-exit": "error",
|
|
8
|
-
"effect-channel/no-throw": "error",
|
|
9
|
-
"effect-channel/no-try-catch": "error"
|
|
10
|
-
}
|
|
11
|
-
}
|