@avi2dg/checks 0.54.1 → 0.55.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 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.55.0
6
+
7
+ Released 2026-10-27.
8
+
9
+ ### Features
10
+
11
+ - **dependencies:** defer the advisory scan to the scheduled run [#10](https://github.com/avi2d/checks/pull/10)
12
+
5
13
  ## 0.54.1
6
14
 
7
15
  Released 2026-10-27.
@@ -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 to `bun.lock`, and every acknowledgement holds |
84
- | 1 | the range adds an advisory no acknowledgement covers, or an acknowledgement does not hold |
85
- | 2 | a ref does not resolve, `advisory-acks.json` does not decode, or no verified scanner or usable database is at hand |
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
- A range that leaves `bun.lock` unchanged runs in under a second, and one that changes it scans for about 10 seconds on a warm cache.
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
 
@@ -12,6 +12,7 @@ It runs the gates under [What runs](../bins.md#what-runs), each in its own proce
12
12
  Gates requiring tracked TypeScript files begin running when the repository tracks TypeScript.
13
13
  `checks-lint-coverage` and `checks-unused` also begin running when the repository tracks an `.astro` file.
14
14
  `checks-advisories` begins running when the repository tracks `bun.lock`.
15
+ `checks-lint` defers its scan to the scheduled run, so the desk and the pull request fetch no database.
15
16
  `checks-frontend-syntax` begins running when the repository tracks `frontend-syntax.json` at its root.
16
17
  `checks-copy` begins running when the repository tracks `copy.json` at its root.
17
18
  `checks-css-size` begins running when the repository tracks a `*.css` file.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avi2dg/checks",
3
- "version": "0.54.1",
3
+ "version": "0.55.0",
4
4
  "description": "Deterministic checks shared across a set of TypeScript repositories",
5
5
  "author": {
6
6
  "name": "avi2d",
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
- { bin: "checks-advisories", vector: "dependencies", file: "advisories.ts", reads: "range", appliesTo: BUN_LOCKFILE },
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
@@ -154,8 +154,9 @@ const runGate = Effect.fn("runGate")(function* (gate: KitGate, range: Range) {
154
154
  const script = (yield* Path.Path).join(import.meta.dir, "..", gate.vector, gate.file);
155
155
  const program = gate.file.endsWith(".sh") ? "sh" : process.execPath;
156
156
  const args = gate.reads === "range" ? [script, ...range.refs] : [script, ...(gate.args ?? [])];
157
+ const extra = gate.env === undefined ? {} : { env: gate.env, extendEnv: true };
157
158
  const exitCode = yield* spawner
158
- .exitCode(ChildProcess.make(program, args, { stdin: "ignore", stdout: "inherit", stderr: "inherit" }))
159
+ .exitCode(ChildProcess.make(program, args, { stdin: "ignore", stdout: "inherit", stderr: "inherit", ...extra }))
159
160
  .pipe(
160
161
  Effect.catch((cause) =>
161
162
  Console.error(`${NAME}: cannot run ${gate.bin}: ${cause.message}`).pipe(Effect.as(ChildProcessSpawner.ExitCode(2))),
@@ -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 outcomeOf = Effect.fn("outcomeOf")(function* (request: Request, root: string, clock: AcknowledgementClock) {
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