@avi2dg/checks 0.66.0 → 0.67.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.67.0
|
|
6
|
+
|
|
7
|
+
Released 2026-11-06.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **ci-wiring:** accept shared runner pick and setup action [#48](https://github.com/avi2d/checks/pull/48)
|
|
12
|
+
|
|
5
13
|
## 0.66.0
|
|
6
14
|
|
|
7
15
|
Released 2026-11-05.
|
|
@@ -68,6 +68,10 @@ Any other expression refuses as unresolvable.
|
|
|
68
68
|
A job on `push` or another trigger no fork reaches may still name self-hosted labels.
|
|
69
69
|
In a private repository each mutation job carries exactly the labels the consumer declares in `package.json` under `checks.mutationRunner`.
|
|
70
70
|
Every other job there carries exactly the hosted-default expression, or exactly one of the label sets the consumer declares under `checks.selfHostedRunners`.
|
|
71
|
+
A job there also carries `runs-on: ${{ needs.<job>.outputs.runs-on }}` when the same workflow needs that job and that job calls the runner pick.
|
|
72
|
+
The pick is the repository file `runner.yml` under `.github/workflows/` or `avi2d/workflows/.github/workflows/runner.yml` at a pinned full SHA.
|
|
73
|
+
A public repository still refuses that expression on a job a fork can reach.
|
|
74
|
+
The resolved label cannot be shown to be a hosted runner before the run.
|
|
71
75
|
A repository without those declarations fails each job they would pin until it declares them.
|
|
72
76
|
The check knows a repository is private only from GitHub's event, so a run outside CI judges only the rules that hold in either.
|
|
73
77
|
|
|
@@ -90,7 +94,9 @@ The fleet version is the version the kit ships in its own `.bun-version`.
|
|
|
90
94
|
The step fails the check when the version is missing or differs from the fleet bun version.
|
|
91
95
|
A range fails because it differs from the fleet version.
|
|
92
96
|
A version built from an expression fails because it cannot be shown equal before the run.
|
|
93
|
-
|
|
97
|
+
A step that uses `avi2d/workflows/actions/setup` reads `.bun-version`.
|
|
98
|
+
The step fails the check when `.bun-version` is missing or differs from the fleet bun version.
|
|
99
|
+
This rule skips a repository with no setup-bun step and no shared setup step.
|
|
94
100
|
A kit release that moves the fleet version turns each consumer red until its pin follows.
|
|
95
101
|
Each refusal names the workflow file and line, the version found and where it was read, and the fleet version.
|
|
96
102
|
|
|
@@ -133,8 +139,10 @@ Each refusal names the workflow file, the job and the limit.
|
|
|
133
139
|
## What it reads
|
|
134
140
|
|
|
135
141
|
The bin reads `.github/workflows/*.yml`, `*.yaml`, the `scripts`, `private` and `checks` runners in `package.json` and the shell scripts those steps and scripts run from the working tree.
|
|
142
|
+
It reads each shell script that a shell script it reads runs, until none is left.
|
|
136
143
|
It lists the `*.ts` and `*.tsx` files `git ls-files` tracks to decide whether the night run applies.
|
|
137
144
|
It reads each file a setup-bun step names for its version.
|
|
145
|
+
It reads `.bun-version` for each step that uses the shared setup action.
|
|
138
146
|
It reads the fleet pin from the kit's own `.bun-version`.
|
|
139
147
|
It reads the target branch from `GITHUB_BASE_REF`, then from `refs/remotes/origin/HEAD` and then from the event file `GITHUB_EVENT_PATH` names.
|
|
140
148
|
It reads whether the repository is private from `repository.private` in that event file.
|
|
@@ -149,7 +157,7 @@ It takes no arguments.
|
|
|
149
157
|
| Code | Result |
|
|
150
158
|
| --- | --- |
|
|
151
159
|
| 0 | Every required command has a reachable step. |
|
|
152
|
-
| 1 | A required command is missing or blocked, a job runs on the wrong runner, a workflow names a missing workflow or job, a
|
|
160
|
+
| 1 | A required command is missing or blocked, a job runs on the wrong runner, a workflow names a missing workflow or job, a bun setup step drifts from the fleet bun version, a workflow that releases runs on a schedule outside a public npm package or more than once a day, a workflow other than a weekly public advisory scan runs on a schedule, a job can run for more than 60 minutes, or a repository that tracks TypeScript never runs `checks-lint --night`. |
|
|
153
161
|
| 2 | A workflow or `package.json` cannot be decoded, or the kit's own `.bun-version` is missing, unreadable or blank. |
|
|
154
162
|
|
|
155
163
|
## Sample output
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avi2dg/checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.0",
|
|
4
4
|
"description": "Deterministic checks shared across a set of TypeScript repositories",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "avi2d",
|
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
"src/delivery/mutation-job.ts",
|
|
82
82
|
"src/delivery/job-timeout.ts",
|
|
83
83
|
"src/delivery/runner-policy.ts",
|
|
84
|
+
"src/delivery/runner-pick.ts",
|
|
84
85
|
"src/delivery/secrets.ts",
|
|
85
86
|
"src/delivery/secrets-pack.ts",
|
|
86
87
|
"src/delivery/pr-text.ts",
|
package/src/delivery/bun-pin.ts
CHANGED
|
@@ -17,12 +17,19 @@ export type BunPinFault = {
|
|
|
17
17
|
|
|
18
18
|
const SETUP_BUN = /^oven-sh\/setup-bun(?:@(?<ref>.*))?$/i;
|
|
19
19
|
const V2_REF = /^v2(?:\.\d+){0,2}$/;
|
|
20
|
+
const SHARED_SETUP = /^avi2d\/workflows\/actions\/setup(?:@(?<ref>.*))?$/i;
|
|
21
|
+
const SETUP_VERSION_FILE = ".bun-version";
|
|
20
22
|
|
|
21
23
|
function setupBunRef(uses: unknown): string | undefined {
|
|
22
24
|
const match = typeof uses === "string" ? SETUP_BUN.exec(uses.trim()) : null;
|
|
23
25
|
return match === null ? undefined : (match.groups?.["ref"] ?? "");
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
function sharedSetupRef(uses: unknown): string | undefined {
|
|
29
|
+
const match = typeof uses === "string" ? SHARED_SETUP.exec(uses.trim()) : null;
|
|
30
|
+
return match === null ? undefined : (match.groups?.["ref"] ?? "");
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
function isExpression(value: string): boolean {
|
|
27
34
|
return value.includes("${{");
|
|
28
35
|
}
|
|
@@ -106,6 +113,15 @@ function unknownRefFault(ref: string): string {
|
|
|
106
113
|
return `uses oven-sh/setup-bun at ${ref === "" ? "no ref" : ref}, but the check knows only setup-bun v2's version resolution, so the step must use oven-sh/setup-bun@v2`;
|
|
107
114
|
}
|
|
108
115
|
|
|
116
|
+
function sharedSetupFault(policy: BunPinPolicy, ref: string): string | undefined {
|
|
117
|
+
const version = nonEmpty(policy.files.get(SETUP_VERSION_FILE));
|
|
118
|
+
const uses = ref === "" ? "avi2d/workflows/actions/setup" : `avi2d/workflows/actions/setup@${ref}`;
|
|
119
|
+
if (version === undefined) {
|
|
120
|
+
return `uses ${uses}, which reads ${SETUP_VERSION_FILE}, but the repository holds no ${SETUP_VERSION_FILE} file, want ${policy.fleet}`;
|
|
121
|
+
}
|
|
122
|
+
return version === policy.fleet ? undefined : `requests bun ${version} from ${SETUP_VERSION_FILE}, want ${policy.fleet}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
109
125
|
export function findBunPinFaults(policy: BunPinPolicy, workflows: readonly Workflow[]): readonly BunPinFault[] {
|
|
110
126
|
return workflows.flatMap((workflow) => {
|
|
111
127
|
const at = locate(workflow.path, workflow.text.split("\n"));
|
|
@@ -113,17 +129,25 @@ export function findBunPinFaults(policy: BunPinPolicy, workflows: readonly Workf
|
|
|
113
129
|
const steps = Array.isArray(job["steps"]) ? job["steps"] : [];
|
|
114
130
|
return steps.flatMap((step: unknown, index: number) => {
|
|
115
131
|
if (!isRecord(step)) return [];
|
|
132
|
+
const atStep = (fault: string) => [
|
|
133
|
+
{ location: at(["jobs", id, "steps", index, "uses"], String(step["uses"]), `job ${id} step ${index + 1}`), fault },
|
|
134
|
+
];
|
|
116
135
|
const ref = setupBunRef(step["uses"]);
|
|
117
|
-
if (ref
|
|
118
|
-
|
|
119
|
-
|
|
136
|
+
if (ref !== undefined) {
|
|
137
|
+
const fault = V2_REF.test(ref) ? bunPinFault(policy, step) : unknownRefFault(ref);
|
|
138
|
+
return fault === undefined ? [] : atStep(fault);
|
|
139
|
+
}
|
|
140
|
+
const sharedRef = sharedSetupRef(step["uses"]);
|
|
141
|
+
if (sharedRef === undefined) return [];
|
|
142
|
+
const fault = sharedSetupFault(policy, sharedRef);
|
|
143
|
+
return fault === undefined ? [] : atStep(fault);
|
|
120
144
|
});
|
|
121
145
|
});
|
|
122
146
|
});
|
|
123
147
|
}
|
|
124
148
|
|
|
125
149
|
export function formatBunPinReport(faults: readonly BunPinFault[]): string {
|
|
126
|
-
return [`ci-wiring: ${faults.length}
|
|
150
|
+
return [`ci-wiring: ${faults.length} bun setup step(s) drift from the fleet bun version:`, ...faults.map(({ location, fault }) => ` ${location}: ${fault}`)].join("\n");
|
|
127
151
|
}
|
|
128
152
|
|
|
129
153
|
function versionFileKeys(job: Readonly<Record<string, unknown>>): readonly string[] {
|
|
@@ -139,11 +163,17 @@ function versionFileKeys(job: Readonly<Record<string, unknown>>): readonly strin
|
|
|
139
163
|
return keys;
|
|
140
164
|
}
|
|
141
165
|
|
|
166
|
+
function usesSharedSetup(job: Readonly<Record<string, unknown>>): boolean {
|
|
167
|
+
const steps = Array.isArray(job["steps"]) ? job["steps"] : [];
|
|
168
|
+
return steps.some((step) => isRecord(step) && sharedSetupRef(step["uses"]) !== undefined);
|
|
169
|
+
}
|
|
170
|
+
|
|
142
171
|
function wantedBunPinFiles(workflows: readonly Workflow[]): ReadonlySet<string> {
|
|
143
172
|
const wanted = new Set<string>([ROOT_MANIFEST]);
|
|
144
173
|
for (const workflow of workflows) {
|
|
145
174
|
for (const [, job] of workflowJobs(workflow)) {
|
|
146
175
|
for (const key of versionFileKeys(job)) wanted.add(key);
|
|
176
|
+
if (usesSharedSetup(job)) wanted.add(SETUP_VERSION_FILE);
|
|
147
177
|
}
|
|
148
178
|
}
|
|
149
179
|
return wanted;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { names, type Workflow } from "./workflow-references.ts";
|
|
2
|
+
|
|
3
|
+
const NEEDS_RUNS_ON = /^\$\{\{\s*needs\.([\w-]+)\.outputs\.runs-on\s*\}\}$/i;
|
|
4
|
+
const SHARED_RUNNER = /^avi2d\/workflows\/\.github\/workflows\/runner\.yml@[0-9a-f]{40}$/i;
|
|
5
|
+
const LOCAL_RUNNER = ".github/workflows/runner.yml";
|
|
6
|
+
|
|
7
|
+
function callsRunnerPick(uses: unknown, workflows: readonly Workflow[]): boolean {
|
|
8
|
+
if (typeof uses !== "string") return false;
|
|
9
|
+
const target = uses.trim();
|
|
10
|
+
if (target.startsWith("./")) {
|
|
11
|
+
const path = target.slice(2);
|
|
12
|
+
return path === LOCAL_RUNNER && workflows.some((workflow) => workflow.path === path);
|
|
13
|
+
}
|
|
14
|
+
return SHARED_RUNNER.test(target);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// GitHub fills needs.<job> only for jobs the current job needs.
|
|
18
|
+
export function readsRunnerPick(
|
|
19
|
+
job: Readonly<Record<string, unknown>>,
|
|
20
|
+
runsOn: unknown,
|
|
21
|
+
jobs: ReadonlyMap<string, Readonly<Record<string, unknown>>>,
|
|
22
|
+
workflows: readonly Workflow[],
|
|
23
|
+
): boolean {
|
|
24
|
+
if (typeof runsOn !== "string") return false;
|
|
25
|
+
const need = NEEDS_RUNS_ON.exec(runsOn.trim())?.[1];
|
|
26
|
+
if (need === undefined) return false;
|
|
27
|
+
if (!(names(job["needs"]) ?? []).includes(need)) return false;
|
|
28
|
+
return callsRunnerPick(jobs.get(need)?.["uses"], workflows);
|
|
29
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Effect, FileSystem, Path, Schema } from "effect";
|
|
2
2
|
import { runsMutation, within, type Run } from "./mutation-job.ts";
|
|
3
|
+
import { readsRunnerPick } from "./runner-pick.ts";
|
|
3
4
|
import { isRecord, names, workflowJobs, workflowTitle, type Workflow } from "./workflow-references.ts";
|
|
4
5
|
|
|
5
6
|
export type RunnerConfig = {
|
|
@@ -314,11 +315,20 @@ function watchedWorkflows(workflow: Workflow): readonly string[] {
|
|
|
314
315
|
return isRecord(watched) ? (names(watched["workflows"]) ?? []) : [];
|
|
315
316
|
}
|
|
316
317
|
|
|
317
|
-
function runnerFault(
|
|
318
|
+
function runnerFault(
|
|
319
|
+
job: Readonly<Record<string, unknown>>,
|
|
320
|
+
runsOn: unknown,
|
|
321
|
+
mutation: boolean,
|
|
322
|
+
policy: RunnerPolicy,
|
|
323
|
+
reachable: boolean,
|
|
324
|
+
jobs: ReadonlyMap<string, Readonly<Record<string, unknown>>>,
|
|
325
|
+
workflows: readonly Workflow[],
|
|
326
|
+
): string | undefined {
|
|
318
327
|
if (policy.visibility === "public" && reachable) {
|
|
319
328
|
const fault = publicFault(job, runsOn, policy.runners);
|
|
320
329
|
if (fault !== undefined) return fault;
|
|
321
330
|
}
|
|
331
|
+
if (readsRunnerPick(job, runsOn, jobs, workflows)) return undefined;
|
|
322
332
|
return mutation ? mutationFault(runsOn, policy.runners.mutation, policy.visibility, reachable) : otherFault(runsOn, policy.runners, policy.visibility);
|
|
323
333
|
}
|
|
324
334
|
|
|
@@ -359,14 +369,16 @@ export function findRunnerFaults(policy: RunnerPolicy, workflows: readonly Workf
|
|
|
359
369
|
}
|
|
360
370
|
}
|
|
361
371
|
}
|
|
362
|
-
return workflows.flatMap((workflow) =>
|
|
363
|
-
workflowJobs(workflow)
|
|
372
|
+
return workflows.flatMap((workflow) => {
|
|
373
|
+
const entries = workflowJobs(workflow);
|
|
374
|
+
const jobs = new Map(entries);
|
|
375
|
+
return entries.flatMap(([id, job]) => {
|
|
364
376
|
if (job["runs-on"] === undefined) return [];
|
|
365
377
|
const mutation = jobRuns(workflow, job).some((run) => runsMutation(run, policy.scripts, (file) => policy.files?.get(file)));
|
|
366
|
-
const fault = runnerFault(job, job["runs-on"], mutation, policy, reachable.get(workflow.path) ?? forkReachable(workflow));
|
|
378
|
+
const fault = runnerFault(job, job["runs-on"], mutation, policy, reachable.get(workflow.path) ?? forkReachable(workflow), jobs, workflows);
|
|
367
379
|
return fault === undefined ? [] : [{ location: workflow.path + " job " + id, fault }];
|
|
368
|
-
})
|
|
369
|
-
);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
370
382
|
}
|
|
371
383
|
|
|
372
384
|
export function formatRunnerReport(faults: readonly RunnerFault[]): string {
|