@avi2dg/checks 0.66.0 → 0.68.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/gates/checks-ci-wiring.md +11 -2
- package/package.json +2 -1
- package/src/delivery/bun-pin.ts +33 -4
- package/src/delivery/runner-pick.ts +29 -0
- package/src/delivery/runner-policy.ts +18 -6
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.68.0
|
|
6
|
+
|
|
7
|
+
Released 2026-11-06.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **ci-wiring:** accept short shared refs for runner pick and setup [#49](https://github.com/avi2d/checks/pull/49)
|
|
12
|
+
|
|
13
|
+
## 0.67.0
|
|
14
|
+
|
|
15
|
+
Released 2026-11-06.
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
- **ci-wiring:** accept shared runner pick and setup action [#48](https://github.com/avi2d/checks/pull/48)
|
|
20
|
+
|
|
5
21
|
## 0.66.0
|
|
6
22
|
|
|
7
23
|
Released 2026-11-05.
|
|
@@ -68,6 +68,11 @@ 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 full SHA or at a version tag, `v<major>` or `v<major>.<minor>.<patch>`.
|
|
73
|
+
A branch name or a missing ref is not the pick.
|
|
74
|
+
A public repository still refuses that expression on a job a fork can reach.
|
|
75
|
+
The resolved label cannot be shown to be a hosted runner before the run.
|
|
71
76
|
A repository without those declarations fails each job they would pin until it declares them.
|
|
72
77
|
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
78
|
|
|
@@ -90,7 +95,9 @@ The fleet version is the version the kit ships in its own `.bun-version`.
|
|
|
90
95
|
The step fails the check when the version is missing or differs from the fleet bun version.
|
|
91
96
|
A range fails because it differs from the fleet version.
|
|
92
97
|
A version built from an expression fails because it cannot be shown equal before the run.
|
|
93
|
-
|
|
98
|
+
A step that uses `avi2d/workflows/setup` or `avi2d/workflows/actions/setup`, at any ref or none, reads `.bun-version`.
|
|
99
|
+
The step fails the check when `.bun-version` is missing or differs from the fleet bun version.
|
|
100
|
+
This rule skips a repository with no setup-bun step and no shared setup step.
|
|
94
101
|
A kit release that moves the fleet version turns each consumer red until its pin follows.
|
|
95
102
|
Each refusal names the workflow file and line, the version found and where it was read, and the fleet version.
|
|
96
103
|
|
|
@@ -133,8 +140,10 @@ Each refusal names the workflow file, the job and the limit.
|
|
|
133
140
|
## What it reads
|
|
134
141
|
|
|
135
142
|
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.
|
|
143
|
+
It reads each shell script that a shell script it reads runs, until none is left.
|
|
136
144
|
It lists the `*.ts` and `*.tsx` files `git ls-files` tracks to decide whether the night run applies.
|
|
137
145
|
It reads each file a setup-bun step names for its version.
|
|
146
|
+
It reads `.bun-version` for each step that uses the shared setup action.
|
|
138
147
|
It reads the fleet pin from the kit's own `.bun-version`.
|
|
139
148
|
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
149
|
It reads whether the repository is private from `repository.private` in that event file.
|
|
@@ -149,7 +158,7 @@ It takes no arguments.
|
|
|
149
158
|
| Code | Result |
|
|
150
159
|
| --- | --- |
|
|
151
160
|
| 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
|
|
161
|
+
| 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
162
|
| 2 | A workflow or `package.json` cannot be decoded, or the kit's own `.bun-version` is missing, unreadable or blank. |
|
|
154
163
|
|
|
155
164
|
## Sample output
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avi2dg/checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.68.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(?:@.*)?$/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 sharedSetupUses(uses: unknown): string | undefined {
|
|
29
|
+
const text = typeof uses === "string" ? uses.trim() : "";
|
|
30
|
+
return SHARED_SETUP.test(text) ? text : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
function isExpression(value: string): boolean {
|
|
27
34
|
return value.includes("${{");
|
|
28
35
|
}
|
|
@@ -106,6 +113,14 @@ 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, uses: string): string | undefined {
|
|
117
|
+
const version = nonEmpty(policy.files.get(SETUP_VERSION_FILE));
|
|
118
|
+
if (version === undefined) {
|
|
119
|
+
return `uses ${uses}, which reads ${SETUP_VERSION_FILE}, but the repository holds no ${SETUP_VERSION_FILE} file, want ${policy.fleet}`;
|
|
120
|
+
}
|
|
121
|
+
return version === policy.fleet ? undefined : `requests bun ${version} from ${SETUP_VERSION_FILE}, want ${policy.fleet}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
109
124
|
export function findBunPinFaults(policy: BunPinPolicy, workflows: readonly Workflow[]): readonly BunPinFault[] {
|
|
110
125
|
return workflows.flatMap((workflow) => {
|
|
111
126
|
const at = locate(workflow.path, workflow.text.split("\n"));
|
|
@@ -113,17 +128,25 @@ export function findBunPinFaults(policy: BunPinPolicy, workflows: readonly Workf
|
|
|
113
128
|
const steps = Array.isArray(job["steps"]) ? job["steps"] : [];
|
|
114
129
|
return steps.flatMap((step: unknown, index: number) => {
|
|
115
130
|
if (!isRecord(step)) return [];
|
|
131
|
+
const atStep = (fault: string) => [
|
|
132
|
+
{ location: at(["jobs", id, "steps", index, "uses"], String(step["uses"]), `job ${id} step ${index + 1}`), fault },
|
|
133
|
+
];
|
|
116
134
|
const ref = setupBunRef(step["uses"]);
|
|
117
|
-
if (ref
|
|
118
|
-
|
|
119
|
-
|
|
135
|
+
if (ref !== undefined) {
|
|
136
|
+
const fault = V2_REF.test(ref) ? bunPinFault(policy, step) : unknownRefFault(ref);
|
|
137
|
+
return fault === undefined ? [] : atStep(fault);
|
|
138
|
+
}
|
|
139
|
+
const sharedUses = sharedSetupUses(step["uses"]);
|
|
140
|
+
if (sharedUses === undefined) return [];
|
|
141
|
+
const fault = sharedSetupFault(policy, sharedUses);
|
|
142
|
+
return fault === undefined ? [] : atStep(fault);
|
|
120
143
|
});
|
|
121
144
|
});
|
|
122
145
|
});
|
|
123
146
|
}
|
|
124
147
|
|
|
125
148
|
export function formatBunPinReport(faults: readonly BunPinFault[]): string {
|
|
126
|
-
return [`ci-wiring: ${faults.length}
|
|
149
|
+
return [`ci-wiring: ${faults.length} bun setup step(s) drift from the fleet bun version:`, ...faults.map(({ location, fault }) => ` ${location}: ${fault}`)].join("\n");
|
|
127
150
|
}
|
|
128
151
|
|
|
129
152
|
function versionFileKeys(job: Readonly<Record<string, unknown>>): readonly string[] {
|
|
@@ -139,11 +162,17 @@ function versionFileKeys(job: Readonly<Record<string, unknown>>): readonly strin
|
|
|
139
162
|
return keys;
|
|
140
163
|
}
|
|
141
164
|
|
|
165
|
+
function usesSharedSetup(job: Readonly<Record<string, unknown>>): boolean {
|
|
166
|
+
const steps = Array.isArray(job["steps"]) ? job["steps"] : [];
|
|
167
|
+
return steps.some((step) => isRecord(step) && sharedSetupUses(step["uses"]) !== undefined);
|
|
168
|
+
}
|
|
169
|
+
|
|
142
170
|
function wantedBunPinFiles(workflows: readonly Workflow[]): ReadonlySet<string> {
|
|
143
171
|
const wanted = new Set<string>([ROOT_MANIFEST]);
|
|
144
172
|
for (const workflow of workflows) {
|
|
145
173
|
for (const [, job] of workflowJobs(workflow)) {
|
|
146
174
|
for (const key of versionFileKeys(job)) wanted.add(key);
|
|
175
|
+
if (usesSharedSetup(job)) wanted.add(SETUP_VERSION_FILE);
|
|
147
176
|
}
|
|
148
177
|
}
|
|
149
178
|
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 = /^[Aa][Vv][Ii]2[Dd]\/[Ww][Oo][Rr][Kk][Ff][Ll][Oo][Ww][Ss]\/\.github\/workflows\/runner\.yml@(?:[0-9a-fA-F]{40}|v\d+(?:\.\d+\.\d+)?)$/;
|
|
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 {
|