@stll/oxlint-config 0.11.0 → 0.12.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.
@@ -1,2291 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- //#region \0rolldown/runtime.js
4
- var __create = Object.create;
5
- var __defProp = Object.defineProperty;
6
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
- var __getOwnPropNames = Object.getOwnPropertyNames;
8
- var __getProtoOf = Object.getPrototypeOf;
9
- var __hasOwnProp = Object.prototype.hasOwnProperty;
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
- key = keys[i];
13
- if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
14
- get: ((k) => from[k]).bind(null, key),
15
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
- });
17
- }
18
- return to;
19
- };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
21
- value: mod,
22
- enumerable: true
23
- }) : target, mod));
24
- //#endregion
3
+ const require_toolchain_guard = require("./toolchain-guard.cjs");
25
4
  let node_fs = require("node:fs");
26
- let node_child_process = require("node:child_process");
27
- let node_path = require("node:path");
28
- node_path = __toESM(node_path, 1);
29
- let picomatch = require("picomatch");
30
- picomatch = __toESM(picomatch, 1);
31
- let smol_toml = require("smol-toml");
32
- let yaml = require("yaml");
33
- let semver = require("semver");
34
- //#region src/toolchain-container-inputs.ts
35
- const record$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
36
- /** Filename class recognized by Dependabot's Docker Compose fetcher. */
37
- const isComposeDefinitionPath = (file) => /^\.?(?:docker-)?compose(?:-[\w]+)?(?:\.[\w-]+)?\.ya?ml$/i.test(file.split("/").at(-1) ?? "");
38
- /** Candidate YAML files are classified by their document shape after reading. */
39
- const isKubernetesDefinitionPath = (file) => /^[^.].*\.ya?ml$/i.test(file.split("/").at(-1) ?? "");
40
- const kubernetesPodSpecPaths = {
41
- Pod: ["spec"],
42
- Deployment: [
43
- "spec",
44
- "template",
45
- "spec"
46
- ],
47
- DaemonSet: [
48
- "spec",
49
- "template",
50
- "spec"
51
- ],
52
- StatefulSet: [
53
- "spec",
54
- "template",
55
- "spec"
56
- ],
57
- ReplicaSet: [
58
- "spec",
59
- "template",
60
- "spec"
61
- ],
62
- ReplicationController: [
63
- "spec",
64
- "template",
65
- "spec"
66
- ],
67
- Job: [
68
- "spec",
69
- "template",
70
- "spec"
71
- ],
72
- CronJob: [
73
- "spec",
74
- "jobTemplate",
75
- "spec",
76
- "template",
77
- "spec"
78
- ]
79
- };
80
- /** Shared semantic image enumeration for runtime checks and update-root generation. */
81
- const containerDocumentImages = (document, file) => {
82
- if (!record$5(document)) return void 0;
83
- const services = document["services"];
84
- if (isComposeDefinitionPath(file) && record$5(services)) {
85
- const images = [];
86
- for (const [name, service] of Object.entries(services)) if (record$5(service) && service["image"] !== void 0) images.push({
87
- image: service["image"],
88
- path: [
89
- "services",
90
- name,
91
- "image"
92
- ]
93
- });
94
- return {
95
- ecosystem: "docker-compose",
96
- images
97
- };
98
- }
99
- if (typeof document["apiVersion"] !== "string" || typeof document["kind"] !== "string") return void 0;
100
- const images = [];
101
- const visited = /* @__PURE__ */ new Set();
102
- const walkResource = (resource, prefix) => {
103
- if (visited.has(resource)) return;
104
- visited.add(resource);
105
- if (typeof resource["apiVersion"] !== "string" || typeof resource["kind"] !== "string") return;
106
- if (resource["kind"] === "List" && Array.isArray(resource["items"])) {
107
- for (const [index, item] of resource["items"].entries()) if (record$5(item)) walkResource(item, [
108
- ...prefix,
109
- "items",
110
- index
111
- ]);
112
- return;
113
- }
114
- const specPath = Object.entries(kubernetesPodSpecPaths).find(([kind]) => kind === resource["kind"])?.[1];
115
- if (specPath === void 0) return;
116
- let spec = resource;
117
- for (const key of specPath) spec = record$5(spec) ? spec[key] : void 0;
118
- if (!record$5(spec)) return;
119
- for (const key of [
120
- "containers",
121
- "initContainers",
122
- "ephemeralContainers"
123
- ]) {
124
- const containers = spec[key];
125
- if (!Array.isArray(containers)) continue;
126
- for (const [index, container] of containers.entries()) if (record$5(container) && container["image"] !== void 0) images.push({
127
- image: container["image"],
128
- path: [
129
- ...prefix,
130
- ...specPath,
131
- key,
132
- index,
133
- "image"
134
- ]
135
- });
136
- }
137
- };
138
- walkResource(document, []);
139
- return images.length === 0 ? void 0 : {
140
- ecosystem: "docker",
141
- images
142
- };
143
- };
144
- //#endregion
145
- //#region src/toolchain-images.ts
146
- /** Docker Hub spellings accepted by the shared runtime policy. */
147
- const dockerHubRegistries = [
148
- "docker.io",
149
- "index.docker.io",
150
- "registry-1.docker.io"
151
- ];
152
- /** Expand the full accepted repository alias class without parsing Dockerfile instructions. */
153
- const ownedDockerImageAliases = (images) => {
154
- const aliases = /* @__PURE__ */ new Set();
155
- for (const image of images) for (const repository of [image, `library/${image}`]) {
156
- aliases.add(repository);
157
- for (const registry of dockerHubRegistries) aliases.add(`${registry}/${repository}`);
158
- }
159
- return [...aliases].sort();
160
- };
161
- /** Preserve tag/digest spelling while normalizing registry and namespace aliases. */
162
- const canonicalDockerRuntime = (image) => {
163
- const registry = dockerHubRegistries.find((entry) => image.toLowerCase().startsWith(`${entry}/`));
164
- return (registry === void 0 ? image : image.slice(registry.length + 1)).replace(/^library\//i, "");
165
- };
166
- //#endregion
167
- //#region src/toolchain-inputs.ts
168
- const excludedInputPath = (file) => file === "" || file.split("/").some((part) => part === "node_modules" || part === "vendor");
169
- /** Match project, environment, local, and fragment filenames recognized by mise. */
170
- const isMiseConfigPath = (file) => !excludedInputPath(file) && (/(?:^|\/)\.?mise(?:\.[^./][^/]*)?\.toml$/.test(file) || /(?:^|\/)(?:\.?mise|\.config\/mise)\/config(?:\.[^./][^/]*)?\.toml$/.test(file) || /(?:^|\/)(?:\.?mise|\.config\/mise)\/conf\.d\/[^./][^/]*\.toml$/.test(file));
171
- /** GitHub action metadata may live at the repository root or in any directory. */
172
- const githubAutomationFileKind = (file) => {
173
- if (file === "") return void 0;
174
- if (/^\.github\/workflows\/[^/]+\.ya?ml$/.test(file)) return "workflow";
175
- if (/(?:^|\/)action\.ya?ml$/.test(file)) return "action";
176
- };
177
- /** Dependabot updates workflows and repository-root action metadata. */
178
- const isDependabotGithubActionsPath = (file) => {
179
- const kind = githubAutomationFileKind(file);
180
- return kind === "workflow" || kind === "action" && !file.includes("/");
181
- };
182
- /** Use one filename discriminator for reader selection and Python ecosystem decisions. */
183
- const pythonDependencyManifestKind = (file) => {
184
- if (excludedInputPath(file)) return void 0;
185
- const name = file.split("/").at(-1);
186
- if (name === "pyproject.toml") return "project";
187
- if (name === "Pipfile") return "pipfile";
188
- if (name === "Pipfile.lock") return "pipfile-lock";
189
- if (name === "setup.py" || name === "setup.cfg") return "setup";
190
- if (name !== void 0 && /^requirements[^/]*\.(?:txt|in)$/.test(name)) return "requirements";
191
- };
192
- const isPythonDependencyManifest = (file) => pythonDependencyManifestKind(file) !== void 0;
193
- /** Docker definition names use dot-delimited variants, excluding source and documentation. */
194
- const isDockerDefinitionPath = (file) => {
195
- if (excludedInputPath(file)) return false;
196
- const name = file.split("/").at(-1) ?? "";
197
- if (/\.(?:[cm]?[jt]sx?|mdx?|rst|txt|html?|py|rs|go|java|sh|c|cc|cpp|cxx|h|hpp|cs|rb|php|swift|kt|css|scss)$/i.test(name)) return false;
198
- return /^(?:[^./]+\.)*(?:dockerfile|containerfile)(?:\.[^./]+)*$/i.test(name);
199
- };
200
- /** Lockfiles establish ecosystem presence without reading their dependency graphs. */
201
- const toolchainInputKind = (file) => {
202
- if (githubAutomationFileKind(file) !== void 0) return "config";
203
- if (excludedInputPath(file)) return void 0;
204
- if (pythonDependencyManifestKind(file) === "pipfile-lock" || /(?:^|\/)(?:bun\.lock|uv\.lock)$/.test(file)) return "presence";
205
- if (githubAutomationFileKind(file) !== void 0 || isMiseConfigPath(file) || isPythonDependencyManifest(file) || isDockerDefinitionPath(file) || isComposeDefinitionPath(file) || isKubernetesDefinitionPath(file) || /(?:^|\/)(?:package\.json|pnpm-workspace\.yaml|stll-toolchain\.json|\.bun-version|\.node-version|\.nvmrc|\.python-version|rust-toolchain(?:\.toml)?|\.tool-versions|\.?mise\.toml|uv\.toml|\.uv\.toml|\.github\/dependabot\.ya?ml|Cargo\.toml)$/.test(file)) return "config";
206
- };
207
- //#endregion
208
- //#region src/toolchain-dependabot.ts
209
- const dependabotRules = ["dependabot-policy"];
210
- const record$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
211
- const strings = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
212
- const sameStrings = (value, expected) => {
213
- if (!strings(value) || value.length !== expected.length) return false;
214
- const sortedExpected = [...expected].sort();
215
- return [...value].sort().every((entry, index) => entry === sortedExpected[index]);
216
- };
217
- const directoryOf = (file) => {
218
- const directory = node_path.default.posix.dirname(file);
219
- return directory === "." ? "/" : `/${directory}`;
220
- };
221
- const workspaceManifests = {
222
- npm: "package.json",
223
- cargo: "Cargo.toml",
224
- uv: "pyproject.toml"
225
- };
226
- const workspaceTable = (parsed, ecosystem) => {
227
- switch (ecosystem) {
228
- case "npm": return parsed["workspaces"];
229
- case "cargo": return parsed["workspace"];
230
- case "uv": {
231
- const tool = parsed["tool"];
232
- const uv = record$4(tool) ? tool["uv"] : void 0;
233
- return record$4(uv) ? uv["workspace"] : void 0;
234
- }
235
- default: throw new Error("unknown workspace ecosystem", { cause: ecosystem });
236
- }
237
- };
238
- /** Workspace members share their update root; independent manifests keep their own. */
239
- const workspaceRoots = (files, ecosystem) => {
240
- const manifests = Object.keys(files).filter((file) => node_path.default.posix.basename(file) === workspaceManifests[ecosystem] || ecosystem === "npm" && node_path.default.posix.basename(file) === "pnpm-workspace.yaml");
241
- const workspaces = /* @__PURE__ */ new Map();
242
- for (const file of manifests) {
243
- if (ecosystem === "npm") {
244
- const directory = node_path.default.posix.dirname(file);
245
- const workspaceFile = node_path.default.posix.join(directory, "pnpm-workspace.yaml");
246
- const text = files[workspaceFile];
247
- if (text !== void 0) {
248
- const document = (0, yaml.parseDocument)(text);
249
- if (document.errors.length > 0) throw new Error(`${workspaceFile}:1: invalid pnpm workspace YAML`);
250
- const parsed = document.toJS({ maxAliasCount: 100 });
251
- if (!record$4(parsed)) throw new Error(`${workspaceFile}:1: pnpm workspace must contain an object`);
252
- const patterns = parsed["packages"] ?? [];
253
- if (!strings(patterns)) throw new Error(`${workspaceFile}:1: pnpm packages must be a string array`);
254
- workspaces.set(directory, patterns.map((pattern) => pattern.startsWith("!") ? `!${node_path.default.posix.normalize(pattern.slice(1))}` : node_path.default.posix.normalize(pattern)));
255
- continue;
256
- }
257
- }
258
- try {
259
- const parsed = ecosystem === "npm" ? JSON.parse(files[file] ?? "") : (0, smol_toml.parse)(files[file] ?? "");
260
- if (!record$4(parsed)) continue;
261
- if (ecosystem === "npm" && typeof parsed["packageManager"] === "string" && parsed["packageManager"].startsWith("pnpm@")) continue;
262
- const value = workspaceTable(parsed, ecosystem);
263
- const patterns = record$4(value) ? value[ecosystem === "npm" ? "packages" : "members"] : value;
264
- const members = ecosystem === "cargo" && record$4(value) && patterns === void 0 ? [] : patterns;
265
- if (strings(members)) {
266
- const excluded = ecosystem !== "npm" && record$4(value) && strings(value["exclude"]) ? value["exclude"].map((pattern) => `!${pattern}`) : [];
267
- workspaces.set(node_path.default.posix.dirname(file), [...members, ...excluded]);
268
- }
269
- } catch {}
270
- }
271
- const implicitCargoMembers = /* @__PURE__ */ new Map();
272
- if (ecosystem === "cargo") for (const [root, patterns] of workspaces) {
273
- const includes = patterns.filter((pattern) => !pattern.startsWith("!"));
274
- const excludes = patterns.filter((pattern) => pattern.startsWith("!"));
275
- const memberDirectories = /* @__PURE__ */ new Set([root]);
276
- for (const file of manifests) {
277
- const directory = node_path.default.posix.dirname(file);
278
- const relative = node_path.default.posix.relative(root, directory);
279
- if (includes.some((pattern) => picomatch.default.isMatch(relative, pattern)) && !excludes.some((pattern) => picomatch.default.isMatch(relative, pattern.slice(1)))) memberDirectories.add(directory);
280
- }
281
- for (const directory of memberDirectories) try {
282
- const parsed = (0, smol_toml.parse)(files[node_path.default.posix.join(directory, "Cargo.toml")] ?? "");
283
- const workspace = (0, smol_toml.parse)(files[node_path.default.posix.join(root, "Cargo.toml")] ?? "")["workspace"];
284
- const inherited = record$4(workspace) ? workspace["dependencies"] : void 0;
285
- const dependencyOwners = [parsed];
286
- const targets = parsed["target"];
287
- if (record$4(targets)) {
288
- for (const target of Object.values(targets)) if (record$4(target)) dependencyOwners.push(target);
289
- }
290
- for (const owner of dependencyOwners) for (const key of [
291
- "dependencies",
292
- "dev-dependencies",
293
- "build-dependencies"
294
- ]) {
295
- const dependencies = owner[key];
296
- if (!record$4(dependencies)) continue;
297
- for (const [name, dependency] of Object.entries(dependencies)) {
298
- if (!record$4(dependency)) continue;
299
- const isInherited = dependency["workspace"] === true;
300
- const resolved = isInherited && record$4(inherited) ? inherited[name] : dependency;
301
- if (!record$4(resolved) || typeof resolved["path"] !== "string") continue;
302
- if (node_path.default.posix.isAbsolute(resolved["path"]) || /\\|\$|^[A-Za-z]:/.test(resolved["path"])) continue;
303
- const target = node_path.default.posix.normalize(node_path.default.posix.join(isInherited ? root : directory, resolved["path"]));
304
- const relative = node_path.default.posix.relative(root, target);
305
- if (relative.startsWith("../") || relative === ".." || files[node_path.default.posix.join(target, "Cargo.toml")] === void 0 || excludes.some((pattern) => picomatch.default.isMatch(relative, pattern.slice(1)))) continue;
306
- memberDirectories.add(target);
307
- }
308
- }
309
- } catch {}
310
- implicitCargoMembers.set(root, memberDirectories);
311
- }
312
- return manifests.filter((file) => {
313
- const directory = node_path.default.posix.dirname(file);
314
- return ![...workspaces].some(([root, patterns]) => {
315
- const relative = node_path.default.posix.relative(root, directory);
316
- if (relative === "" || relative.startsWith("../")) return false;
317
- const includes = patterns.filter((pattern) => !pattern.startsWith("!"));
318
- const excludes = patterns.filter((pattern) => pattern.startsWith("!"));
319
- return (implicitCargoMembers.get(root)?.has(directory) === true || includes.some((pattern) => picomatch.default.isMatch(relative, pattern))) && !excludes.some((pattern) => picomatch.default.isMatch(relative, pattern.slice(1)));
320
- });
321
- }).map(directoryOf).filter((directory, index, values) => values.indexOf(directory) === index);
322
- };
323
- const javascriptEcosystem = (directory, files) => {
324
- const root = directory === "/" ? "." : directory.slice(1);
325
- if (files[node_path.default.posix.join(root, "bun.lock")] !== void 0) return "bun";
326
- try {
327
- const manifest = JSON.parse(files[node_path.default.posix.join(root, "package.json")] ?? "{}");
328
- if (record$4(manifest) && typeof manifest["packageManager"] === "string" && manifest["packageManager"].startsWith("bun@")) return "bun";
329
- } catch {}
330
- return "npm";
331
- };
332
- const pythonEcosystem = (file, files) => {
333
- const manifestKind = pythonDependencyManifestKind(file);
334
- if (manifestKind === "pipfile" || manifestKind === "pipfile-lock" || manifestKind === "setup") return "pip";
335
- const root = node_path.default.posix.dirname(file);
336
- if (files[node_path.default.posix.join(root, "uv.lock")] !== void 0 || files[node_path.default.posix.join(root, "uv.toml")] !== void 0) return "uv";
337
- const pyproject = files[node_path.default.posix.join(root, "pyproject.toml")];
338
- if (pyproject !== void 0) try {
339
- const tool = (0, smol_toml.parse)(pyproject)["tool"];
340
- if (record$4(tool) && record$4(tool["uv"])) return "uv";
341
- } catch {}
342
- return "pip";
343
- };
344
- const ecosystemRoots = (files) => {
345
- const roots = /* @__PURE__ */ new Map();
346
- const requireRoot = (ecosystem, directory) => {
347
- const directories = roots.get(ecosystem) ?? /* @__PURE__ */ new Set();
348
- directories.add(directory);
349
- roots.set(ecosystem, directories);
350
- };
351
- for (const directory of workspaceRoots(files, "npm")) requireRoot(javascriptEcosystem(directory, files), directory);
352
- for (const directory of workspaceRoots(files, "cargo")) requireRoot("cargo", directory);
353
- for (const directory of workspaceRoots(files, "uv")) {
354
- const root = directory === "/" ? "." : directory.slice(1);
355
- requireRoot(pythonEcosystem(node_path.default.posix.join(root, "pyproject.toml"), files), directory);
356
- }
357
- for (const file of Object.keys(files)) {
358
- if (isDependabotGithubActionsPath(file)) requireRoot("github-actions", "/");
359
- const name = node_path.default.posix.basename(file);
360
- if (isDockerDefinitionPath(file)) requireRoot("docker", directoryOf(file));
361
- if (isComposeDefinitionPath(file) || isKubernetesDefinitionPath(file)) for (const document of (0, yaml.parseAllDocuments)(files[file] ?? "", { merge: true })) {
362
- if (document.errors.length > 0) continue;
363
- try {
364
- const classified = containerDocumentImages(document.toJS({ maxAliasCount: 100 }), file);
365
- if (classified !== void 0) requireRoot(classified.ecosystem, directoryOf(file));
366
- } catch {}
367
- }
368
- if (name === "uv.lock" || name === "uv.toml" || name !== "pyproject.toml" && isPythonDependencyManifest(file)) requireRoot(pythonEcosystem(file, files), directoryOf(file));
369
- }
370
- return roots;
371
- };
372
- const ignoredFor = (ecosystem, policy) => {
373
- if (ecosystem === "npm" || ecosystem === "bun") return policy.ignoredPackages;
374
- if (ecosystem === "github-actions") return policy.ignoredActions;
375
- if (ecosystem === "docker" || ecosystem === "docker-compose") return ownedDockerImageAliases(policy.ignoredImages);
376
- return [];
377
- };
378
- /** Validate the shared update policy against a hermetic tracked-file snapshot. */
379
- const checkDependabot = ({ files, policy }) => {
380
- const roots = ecosystemRoots(files);
381
- const paths = [".github/dependabot.yml", ".github/dependabot.yaml"].filter((file) => files[file] !== void 0);
382
- if (roots.size === 0 && paths.length === 0) return [];
383
- const file = paths.at(0) ?? ".github/dependabot.yml";
384
- const diagnostics = [];
385
- const add = (message, line = 1) => {
386
- diagnostics.push({
387
- rule: "dependabot-policy",
388
- path: file,
389
- line,
390
- message
391
- });
392
- };
393
- if (paths.length === 0) {
394
- add("add a Dependabot configuration for the repository's package ecosystems");
395
- return diagnostics;
396
- }
397
- if (paths.length > 1) add("keep one Dependabot configuration file");
398
- const lineCounter = new yaml.LineCounter();
399
- const document = (0, yaml.parseDocument)(files[file] ?? "", { lineCounter });
400
- if (document.errors.length > 0) {
401
- for (const error of document.errors) add(`invalid Dependabot YAML: ${error.message}`, lineCounter.linePos(error.pos[0]).line);
402
- return diagnostics;
403
- }
404
- let parsed;
405
- try {
406
- parsed = document.toJS({ maxAliasCount: 100 });
407
- } catch {
408
- add("cannot resolve Dependabot YAML aliases");
409
- return diagnostics;
410
- }
411
- if (!record$4(parsed) || parsed["version"] !== 2 || !Array.isArray(parsed["updates"])) {
412
- add("Dependabot must declare version: 2 and an updates array");
413
- return diagnostics;
414
- }
415
- const covered = /* @__PURE__ */ new Map();
416
- let legacy;
417
- try {
418
- const legacyDocument = (0, yaml.parseDocument)(files[file] ?? "", {
419
- version: "1.1",
420
- schema: "yaml-1.1"
421
- });
422
- if (legacyDocument.errors.length === 0) legacy = legacyDocument.toJS({ maxAliasCount: 100 });
423
- } catch {}
424
- const legacyUpdates = record$4(legacy) && Array.isArray(legacy["updates"]) ? legacy["updates"] : [];
425
- const rootClaims = /* @__PURE__ */ new Map();
426
- const directoryClaims = /* @__PURE__ */ new Map();
427
- for (const [index, update] of parsed["updates"].entries()) {
428
- const lineOf = (key) => {
429
- const node = document.getIn([
430
- "updates",
431
- index,
432
- key
433
- ], true);
434
- const offset = (0, yaml.isNode)(node) ? node.range?.[0] : void 0;
435
- return offset === void 0 ? 1 : lineCounter.linePos(offset).line;
436
- };
437
- const report = (key, message) => add(message, lineOf(key));
438
- if (!record$4(update) || typeof update["package-ecosystem"] !== "string") {
439
- report("package-ecosystem", "each Dependabot update must declare a package ecosystem");
440
- continue;
441
- }
442
- const ecosystem = update["package-ecosystem"];
443
- if (update["target-branch"] !== void 0) report("target-branch", `${ecosystem}: shared updates must target the default branch`);
444
- if (update["open-pull-requests-limit"] === 0) report("open-pull-requests-limit", `${ecosystem}: version updates must remain enabled`);
445
- const single = update["directory"];
446
- const multiple = update["directories"];
447
- let directories = [];
448
- if (typeof single === "string" && multiple === void 0) directories = [single];
449
- else if (single === void 0 && strings(multiple)) directories = multiple;
450
- const validDirectories = directories.filter((directory) => directory.startsWith("/") && !directory.split("/").includes(".."));
451
- if (directories.length === 0 || validDirectories.length !== directories.length) report("directory", `${ecosystem}: declare directory or nonempty directories with absolute repository paths`);
452
- const normalizedDirectories = Array.from(new Set(validDirectories.map((value) => node_path.default.posix.normalize(value).replace(/\/+$/, "") || "/")));
453
- const matches = covered.get(ecosystem) ?? /* @__PURE__ */ new Set();
454
- const claimedDirectories = directoryClaims.get(ecosystem) ?? /* @__PURE__ */ new Map();
455
- for (const directory of normalizedDirectories) {
456
- const previous = claimedDirectories.get(directory);
457
- if (previous !== void 0) report(multiple === void 0 ? "directory" : "directories", `${ecosystem}: directory ${directory} overlaps update entry ${previous + 1}`);
458
- else claimedDirectories.set(directory, index);
459
- }
460
- directoryClaims.set(ecosystem, claimedDirectories);
461
- const claimedRoots = rootClaims.get(ecosystem) ?? /* @__PURE__ */ new Map();
462
- for (const root of roots.get(ecosystem) ?? []) if (normalizedDirectories.some((directory) => directory === root || multiple !== void 0 && picomatch.default.isMatch(root, directory))) {
463
- const previous = claimedRoots.get(root);
464
- if (previous !== void 0) report(multiple === void 0 ? "directory" : "directories", `${ecosystem}: update root ${root} overlaps update entry ${previous + 1}`);
465
- else claimedRoots.set(root, index);
466
- matches.add(root);
467
- }
468
- rootClaims.set(ecosystem, claimedRoots);
469
- covered.set(ecosystem, matches);
470
- const schedule = update["schedule"];
471
- const legacyUpdate = legacyUpdates.at(index);
472
- const legacySchedule = record$4(legacyUpdate) ? legacyUpdate["schedule"] : void 0;
473
- if (record$4(schedule) && schedule["time"] === policy.schedule.time && (!record$4(legacySchedule) || legacySchedule["time"] !== policy.schedule.time)) report("schedule", `${ecosystem}: quote schedule time to preserve its string value in YAML 1.1`);
474
- if (!record$4(schedule) || Object.keys(schedule).length !== Object.keys(policy.schedule).length || Object.entries(policy.schedule).some(([key, value]) => schedule[key] !== value)) report("schedule", `${ecosystem}: schedule must match the shared policy`);
475
- const cooldown = update["cooldown"];
476
- if (!record$4(cooldown) || Object.keys(cooldown).length !== 1 || cooldown["default-days"] !== policy.cooldown.defaultDays) report("cooldown", `${ecosystem}: cooldown must contain only default-days: ${policy.cooldown.defaultDays}`);
477
- const groups = update["groups"];
478
- if (!record$4(groups) || Object.keys(groups).length !== Object.keys(policy.groups).length || Object.entries(policy.groups).some(([name, expected]) => {
479
- const group = groups[name];
480
- return !record$4(group) || Object.keys(group).length !== 2 || !sameStrings(group["patterns"], expected.patterns) || !sameStrings(group["update-types"], expected.updateTypes);
481
- })) report("groups", `${ecosystem}: groups must match the shared policy`);
482
- const ignored = ignoredFor(ecosystem, policy);
483
- const ignore = update["ignore"];
484
- if (ignore !== void 0 && (!Array.isArray(ignore) || ignore.length === 0)) report("ignore", `${ecosystem}: omit ignore or provide a nonempty array`);
485
- if (Array.isArray(ignore) && ignore.some((entry) => !record$4(entry) || typeof entry["dependency-name"] !== "string" || !ignored.includes(entry["dependency-name"]))) report("ignore", `${ecosystem}: ignore only packages owned by the shared policy`);
486
- for (const name of ignored) if (!Array.isArray(ignore) || !ignore.some((entry) => record$4(entry) && entry["dependency-name"] === name && Object.keys(entry).length === 1)) report("ignore", `${ecosystem}: ignore all updates for shared pin ${name}`);
487
- }
488
- for (const [ecosystem, directories] of roots) for (const directory of directories) if (!covered.get(ecosystem)?.has(directory)) add(`add a ${ecosystem} update entry covering ${directory}`);
489
- return diagnostics;
490
- };
491
- //#endregion
492
- //#region src/toolchain-node.ts
493
- const numericComponent = "(?:0|[1-9]\\d*)";
494
- const policyPattern = new RegExp(`^${numericComponent}\\.x$`);
495
- const selectorPattern = new RegExp(`^(${numericComponent})(?:\\.(?:x|${numericComponent}(?:\\.${numericComponent})?))?$`);
496
- /** A Node policy selects the complete stable release series of one major. */
497
- const nodePolicyValid = (value) => policyPattern.test(value) && (0, semver.validRange)(value) !== null;
498
- /** Static selectors may narrow that series, but cannot float across majors. */
499
- const nodeSelectorMatches = (value, policy) => {
500
- if (!nodePolicyValid(policy) || typeof value !== "string") return false;
501
- const major = selectorPattern.exec(value)?.[1];
502
- return major !== void 0 && major === policy.slice(0, -2) && (0, semver.validRange)(value) !== null;
503
- };
504
- /** Engines must support every stable release in the approved major series. */
505
- const nodeSupportRangeMatches = (policy, range) => {
506
- if (!nodePolicyValid(policy)) return false;
507
- try {
508
- return (0, semver.subset)(policy, range);
509
- } catch {
510
- return false;
511
- }
512
- };
513
- //#endregion
514
- //#region src/toolchain-packages.ts
515
- const packageRules = [
516
- "bun-pins",
517
- "package-pins",
518
- "typescript-layout",
519
- "node-engine"
520
- ];
521
- const record$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
522
- const shellSubstitutions = (command) => {
523
- const frames = [{
524
- kind: "root",
525
- start: 0,
526
- depth: 0,
527
- quote: void 0
528
- }];
529
- const bodies = [];
530
- for (let index = 0; index < command.length; index += 1) {
531
- const frame = frames.at(-1);
532
- if (frame === void 0) break;
533
- const character = command.at(index);
534
- if (character === "\\" && frame.quote !== "'") {
535
- index += 1;
536
- continue;
537
- }
538
- if (character === "`" && frame.kind === "backticks") {
539
- bodies.push(command.slice(frame.start, index));
540
- frames.pop();
541
- continue;
542
- }
543
- if (character === frame.quote) {
544
- frame.quote = void 0;
545
- continue;
546
- }
547
- if (frame.quote === "'") continue;
548
- if (character === "$" && command.at(index + 1) === "(") {
549
- frames.push({
550
- kind: "parentheses",
551
- start: index + 2,
552
- depth: 1,
553
- quote: void 0
554
- });
555
- index += 1;
556
- continue;
557
- }
558
- if (character === "`") {
559
- frames.push({
560
- kind: "backticks",
561
- start: index + 1,
562
- depth: 0,
563
- quote: void 0
564
- });
565
- continue;
566
- }
567
- if (frame.quote !== void 0) continue;
568
- if (character === "'" || character === "\"") {
569
- frame.quote = character;
570
- continue;
571
- }
572
- if (frame.kind !== "parentheses") continue;
573
- if (character === "(") frame.depth += 1;
574
- if (character === ")" && --frame.depth === 0) {
575
- bodies.push(command.slice(frame.start, index));
576
- frames.pop();
577
- }
578
- }
579
- for (const frame of frames.slice(1)) bodies.push(command.slice(frame.start));
580
- return bodies;
581
- };
582
- const compilerCommandSegments = (command) => {
583
- const segments = [[]];
584
- for (const source of [command, ...shellSubstitutions(command)]) {
585
- segments.push([]);
586
- const shellWords = source.match(/(?:[^\s"'\\;&|()]|\\[^\n]|"(?:[^"\\]|\\.)*"|'[^']*')+|&&|\|\||[();&|\n]/g);
587
- for (const word of shellWords ?? []) if (/^(?:&&|\|\||[;&|\n])$/.test(word)) segments.push([]);
588
- else segments.at(-1)?.push(word);
589
- }
590
- return segments.map((words) => {
591
- let start = 0;
592
- const skipAssignments = () => {
593
- while (/^[A-Za-z_][A-Za-z\d_]*=/.test(words.at(start) ?? "")) start += 1;
594
- };
595
- skipAssignments();
596
- while (/^(?:\/usr\/bin\/|\/bin\/)?env$/.test(words.at(start) ?? "")) {
597
- start += 1;
598
- while (start < words.length) {
599
- const option = words.at(start);
600
- if (option === "--") {
601
- start += 1;
602
- break;
603
- }
604
- if (option === "-u" || option === "--unset") {
605
- start += 2;
606
- continue;
607
- }
608
- if (option === "-" || option === "-i" || option === "--ignore-environment" || /^--unset=.+|^-u.+/.test(option ?? "")) {
609
- start += 1;
610
- continue;
611
- }
612
- break;
613
- }
614
- skipAssignments();
615
- }
616
- return words.slice(start);
617
- });
618
- };
619
- const launcherValueOptions = new Map(Object.entries({
620
- npx: [
621
- "-p",
622
- "--package",
623
- "-c",
624
- "--call",
625
- "--cache",
626
- "--registry",
627
- "--userconfig"
628
- ],
629
- bunx: ["-p", "--package"],
630
- pnpm: [
631
- "--filter",
632
- "-F",
633
- "--dir",
634
- "-C",
635
- "--package"
636
- ],
637
- yarn: [
638
- "--cwd",
639
- "--cache-folder",
640
- "--mutex",
641
- "--use-yarnrc",
642
- "--modules-folder",
643
- "--registry"
644
- ],
645
- bun: [
646
- "--cwd",
647
- "--filter",
648
- "-F",
649
- "--config",
650
- "-c",
651
- "--env-file",
652
- "-e",
653
- "--eval",
654
- "-p",
655
- "--print"
656
- ],
657
- node: [
658
- "-e",
659
- "--eval",
660
- "-p",
661
- "--print",
662
- "--conditions",
663
- "-C",
664
- "--inspect-port",
665
- "--max-old-space-size",
666
- "--stack-size",
667
- "--title",
668
- "--icu-data-dir",
669
- "--openssl-config",
670
- "--input-type"
671
- ]
672
- }).map(([launcher, options]) => [launcher, new Set(options)]));
673
- const invokesCompiler = (words) => {
674
- const literal = (word) => {
675
- let result = "";
676
- let quote;
677
- for (let index = 0; index < word.length; index += 1) {
678
- const character = word.at(index);
679
- if (character === quote) {
680
- quote = void 0;
681
- continue;
682
- }
683
- if (quote === void 0 && (character === "'" || character === "\"")) {
684
- quote = character;
685
- continue;
686
- }
687
- const next = word.at(index + 1);
688
- if (character === "\\" && next !== void 0 && quote !== "'" && (quote === void 0 || /[$`"\\\n]/.test(next))) {
689
- result += next;
690
- index += 1;
691
- } else result += character;
692
- }
693
- return result;
694
- };
695
- const tokens = words.map(literal);
696
- const compiler = (word) => /(?:^|\/)(?:tsc|tsgo)(?:\.js)?$/.test(word);
697
- let start = 0;
698
- const skipOptions = (index, launcher) => {
699
- while (tokens.at(index)?.startsWith("-")) {
700
- const option = tokens.at(index);
701
- index += 1;
702
- if (option === "--") break;
703
- if (option !== void 0 && launcherValueOptions.get(launcher)?.has(option)) index += 1;
704
- }
705
- return index;
706
- };
707
- while (start < tokens.length) {
708
- const launcher = tokens.at(start);
709
- if (launcher === void 0 || !launcherValueOptions.has(launcher)) break;
710
- start = skipOptions(start + 1, launcher);
711
- if (launcher === "bun" && tokens.at(start) === "check") return true;
712
- const subcommand = tokens.at(start);
713
- if (launcher === "bun" && (subcommand === "x" || subcommand === "run") || launcher === "pnpm" && (subcommand === "exec" || subcommand === "dlx") || launcher === "yarn" && (subcommand === "exec" || subcommand === "dlx" || subcommand === "run")) start = skipOptions(start + 1, launcher);
714
- }
715
- return tokens.slice(start).some((word, index) => compiler(word) || word === "bun" && tokens.at(skipOptions(start + index + 1, "bun")) === "check");
716
- };
717
- /** Validate manifests and catalog resolutions from a tracked-file snapshot. */
718
- const checkPackageFiles = ({ files, policy }) => {
719
- const sources = new Map(Object.entries(files));
720
- const diagnostics = [];
721
- const pins = new Map(Object.entries({
722
- ...policy.packages,
723
- "bun-types": policy.bun
724
- }));
725
- const manifests = /* @__PURE__ */ new Map();
726
- const yamlLocations = /* @__PURE__ */ new Map();
727
- const reportedLines = /* @__PURE__ */ new Map();
728
- const add = ({ file, rule, key, value, message }) => {
729
- const source = sources.get(file);
730
- if (source === void 0) throw new Error(`Missing package diagnostic source: ${file}`);
731
- const serialized = value === void 0 ? void 0 : JSON.stringify(value);
732
- const locationKey = `${file}:${key}:${serialized ?? ""}`;
733
- const previous = reportedLines.get(locationKey) ?? -1;
734
- const yamlLine = yamlLocations.get(locationKey)?.shift();
735
- const index = source.split(/\r?\n/).findIndex((line, lineIndex) => lineIndex > previous && line.includes(JSON.stringify(key)) && (serialized === void 0 || line.includes(serialized)));
736
- if (index >= 0) reportedLines.set(locationKey, index);
737
- diagnostics.push({
738
- rule,
739
- path: file,
740
- line: yamlLine ?? Math.max(1, index + 1),
741
- message
742
- });
743
- };
744
- for (const [file, text] of sources) {
745
- const pnpm = node_path.default.posix.basename(file) === "pnpm-workspace.yaml";
746
- if (!pnpm && node_path.default.posix.basename(file) !== "package.json") continue;
747
- try {
748
- const lineCounter = new yaml.LineCounter();
749
- const document = pnpm ? (0, yaml.parseDocument)(text, { lineCounter }) : void 0;
750
- if (document !== void 0 && document.errors.length > 0) throw new Error("invalid YAML");
751
- const json = document === void 0 ? JSON.parse(text) : document.toJS();
752
- if (!record$3(json)) throw new Error("manifest must be an object");
753
- if (pnpm) {
754
- if (json["catalog"] !== void 0 && !record$3(json["catalog"])) throw new Error("catalog must be a mapping");
755
- if (json["catalogs"] !== void 0 && (!record$3(json["catalogs"]) || Object.values(json["catalogs"]).some((catalog) => !record$3(catalog)))) throw new Error("named catalogs must be mappings");
756
- if (json["packages"] !== void 0 && (!Array.isArray(json["packages"]) || !json["packages"].every((pattern) => typeof pattern === "string"))) throw new Error("workspace packages must be patterns");
757
- }
758
- manifests.set(file, json);
759
- if (document === void 0) continue;
760
- const catalogEntries = [
761
- {
762
- catalog: json["catalog"],
763
- prefix: ["catalog"]
764
- },
765
- {
766
- catalog: json["overrides"],
767
- prefix: ["overrides"]
768
- },
769
- ...record$3(json["catalogs"]) ? Object.entries(json["catalogs"]).map(([name, catalog]) => ({
770
- catalog,
771
- prefix: ["catalogs", name]
772
- })) : []
773
- ];
774
- for (const { catalog, prefix } of catalogEntries) {
775
- if (!record$3(catalog)) continue;
776
- for (const [name, value] of Object.entries(catalog)) {
777
- const node = document.getIn([...prefix, name], true);
778
- if (!(0, yaml.isNode)(node) || node.range === void 0 || node.range === null) continue;
779
- const key = `${file}:${name}:${JSON.stringify(value)}`;
780
- const lines = yamlLocations.get(key) ?? [];
781
- lines.push(lineCounter.linePos(node.range[0]).line);
782
- yamlLocations.set(key, lines);
783
- }
784
- }
785
- } catch {
786
- add({
787
- file,
788
- rule: "package-pins",
789
- key: "",
790
- message: pnpm ? "invalid pnpm-workspace.yaml object" : "invalid package.json object"
791
- });
792
- }
793
- }
794
- const catalogSources = (json) => [json, ...record$3(json["workspaces"]) ? [json["workspaces"]] : []];
795
- const pnpmWorkspaceFor = (file) => {
796
- let directory = node_path.default.posix.dirname(file);
797
- while (true) {
798
- const workspace = manifests.get(node_path.default.posix.join(directory, "pnpm-workspace.yaml"));
799
- if (workspace !== void 0) return {
800
- directory,
801
- workspace
802
- };
803
- if (directory === ".") return void 0;
804
- directory = node_path.default.posix.dirname(directory);
805
- }
806
- };
807
- const catalogFor = (file, name) => {
808
- const owner = pnpmWorkspaceFor(file);
809
- if (owner !== void 0) {
810
- const { directory, workspace } = owner;
811
- if (node_path.default.posix.basename(file) === "package.json" && !workspaceContains({
812
- directory,
813
- file,
814
- patterns: workspace["packages"],
815
- rootMembership: "implicit"
816
- })) return void 0;
817
- if (name === "" || name === "default") return workspace["catalog"] ?? (record$3(workspace["catalogs"]) ? workspace["catalogs"]["default"] : void 0);
818
- return record$3(workspace["catalogs"]) ? workspace["catalogs"][name] : void 0;
819
- }
820
- let directory = node_path.default.posix.dirname(file);
821
- while (true) {
822
- const manifest = manifests.get(node_path.default.posix.join(directory, "package.json"));
823
- if (manifest !== void 0) {
824
- for (const source of catalogSources(manifest)) {
825
- if (name === "" && source["catalog"] !== void 0) return source["catalog"];
826
- if (record$3(source["catalogs"]) && source["catalogs"][name] !== void 0) return source["catalogs"][name];
827
- }
828
- if (manifest["workspaces"] !== void 0) return void 0;
829
- }
830
- if (directory === ".") return void 0;
831
- directory = node_path.default.posix.dirname(directory);
832
- }
833
- };
834
- const workspaceContains = ({ directory, file, patterns, rootMembership }) => {
835
- const relative = node_path.default.posix.relative(directory, node_path.default.posix.dirname(file));
836
- if (relative === "" && rootMembership === "implicit") return true;
837
- if (relative === ".." || relative.startsWith("../")) return false;
838
- if (!Array.isArray(patterns)) return false;
839
- const normalize = (pattern) => node_path.default.posix.normalize(pattern);
840
- const selected = patterns.filter((value) => typeof value === "string");
841
- return (0, picomatch.default)(selected.filter((pattern) => !pattern.startsWith("!")).map(normalize))(relative) && !(0, picomatch.default)(selected.filter((pattern) => pattern.startsWith("!")).map((pattern) => normalize(pattern.slice(1))))(relative);
842
- };
843
- const resolve = (file, name, value) => {
844
- const visited = /* @__PURE__ */ new Set();
845
- while (typeof value === "string" && value.startsWith("catalog:")) {
846
- if (visited.has(value)) return void 0;
847
- visited.add(value);
848
- const catalog = catalogFor(file, value.slice(8));
849
- value = record$3(catalog) ? catalog[name] : void 0;
850
- }
851
- return value;
852
- };
853
- const workspaceOwnerFor = (file) => {
854
- const pnpm = pnpmWorkspaceFor(file);
855
- if (pnpm !== void 0) return {
856
- directory: pnpm.directory,
857
- patterns: pnpm.workspace["packages"],
858
- source: "pnpm"
859
- };
860
- let directory = node_path.default.posix.dirname(file);
861
- while (true) {
862
- const workspaces = manifests.get(node_path.default.posix.join(directory, "package.json"))?.["workspaces"];
863
- if (workspaces !== void 0) return {
864
- directory,
865
- patterns: record$3(workspaces) ? workspaces["packages"] : workspaces,
866
- source: "json"
867
- };
868
- if (directory === ".") return void 0;
869
- directory = node_path.default.posix.dirname(directory);
870
- }
871
- };
872
- const workspaceResolution = ({ file, name, pin }) => {
873
- const owner = workspaceOwnerFor(file);
874
- if (owner === void 0) return "nonmember";
875
- const { directory, patterns, source } = owner;
876
- if (source === "json" && !Array.isArray(patterns)) return "mismatch";
877
- if (!workspaceContains({
878
- directory,
879
- file,
880
- patterns,
881
- rootMembership: "implicit"
882
- })) return "nonmember";
883
- const candidates = [...manifests.entries()].filter(([candidate, json]) => node_path.default.posix.basename(candidate) === "package.json" && json["name"] === name && workspaceContains({
884
- directory,
885
- file: candidate,
886
- patterns,
887
- rootMembership: source === "json" ? "patterns" : "implicit"
888
- }));
889
- return candidates.length === 1 && candidates.every(([, json]) => json["version"] === pin) ? "matched" : "mismatch";
890
- };
891
- const tsSpecifiers = /* @__PURE__ */ new Map();
892
- for (const layout of policy.typescriptInstallLayouts) {
893
- const values = tsSpecifiers.get(layout.compilerPackage) ?? /* @__PURE__ */ new Set();
894
- values.add(layout.compilerSpecifier);
895
- tsSpecifiers.set(layout.compilerPackage, values);
896
- if (layout.type !== "split-compatibility") continue;
897
- const compatibility = tsSpecifiers.get(layout.compatibilityPackage) ?? /* @__PURE__ */ new Set();
898
- compatibility.add(layout.compatibilitySpecifier);
899
- tsSpecifiers.set(layout.compatibilityPackage, compatibility);
900
- }
901
- tsSpecifiers.set(policy.typescript6Compatibility.packageAlias, /* @__PURE__ */ new Set([`npm:typescript@${policy.typescript6Compatibility.version}`]));
902
- for (const [file, json] of manifests) {
903
- const manager = json["packageManager"];
904
- if (manager !== void 0 && manager !== `bun@${policy.bun}`) add({
905
- file,
906
- rule: "bun-pins",
907
- key: "packageManager",
908
- value: manager,
909
- message: `packageManager Bun version must be ${policy.bun}, found ${JSON.stringify(manager)}`
910
- });
911
- const dependencies = {};
912
- const checkEntries = (entries, installed, location) => {
913
- if (!record$3(entries)) return;
914
- for (const [name, raw] of Object.entries(entries)) {
915
- const value = resolve(file, name, raw);
916
- if (installed) dependencies[name] = value;
917
- const tsAllowed = tsSpecifiers.get(name);
918
- if (tsAllowed !== void 0) {
919
- if (typeof value !== "string" || !tsAllowed.has(value)) add({
920
- file,
921
- rule: "typescript-layout",
922
- key: location?.key ?? name,
923
- value: location === void 0 ? raw : location.value,
924
- message: `${name} must use a declared TypeScript specifier, found ${String(value)}`
925
- });
926
- continue;
927
- }
928
- const pin = pins.get(name);
929
- if (pin === void 0) continue;
930
- if (value === pin) continue;
931
- const workspace = value === "workspace:*" ? workspaceResolution({
932
- file,
933
- name,
934
- pin
935
- }) : void 0;
936
- if (workspace === "matched") continue;
937
- add({
938
- file,
939
- rule: name === "bun-types" ? "bun-pins" : "package-pins",
940
- key: location?.key ?? name,
941
- value: location === void 0 ? raw : location.value,
942
- message: workspace === "nonmember" ? `${name} workspace:* consumer is not a member of its nearest workspace` : `${name} must be ${pin}, found ${String(value)}`
943
- });
944
- }
945
- };
946
- for (const key of [
947
- "dependencies",
948
- "devDependencies",
949
- "optionalDependencies"
950
- ]) checkEntries(json[key], true);
951
- for (const source of catalogSources(json)) {
952
- checkEntries(source["catalog"], false);
953
- if (!record$3(source["catalogs"])) continue;
954
- for (const catalog of Object.values(source["catalogs"])) checkEntries(catalog, false);
955
- }
956
- const ownedNames = /* @__PURE__ */ new Set([...pins.keys(), ...tsSpecifiers.keys()]);
957
- const resolutionDependencies = {};
958
- const resolutionRoot = manifests.get(node_path.default.posix.join(node_path.default.posix.dirname(file), "package.json"));
959
- if (resolutionRoot !== void 0) for (const section of [
960
- "dependencies",
961
- "devDependencies",
962
- "optionalDependencies"
963
- ]) {
964
- const entries = resolutionRoot[section];
965
- if (!record$3(entries)) continue;
966
- for (const [name, value] of Object.entries(entries)) resolutionDependencies[name] = resolve(file, name, value);
967
- }
968
- const affectedSpecifiers = /* @__PURE__ */ new Map();
969
- const owner = workspaceOwnerFor(file);
970
- for (const [candidate, member] of manifests) {
971
- if (node_path.default.posix.basename(candidate) !== "package.json") continue;
972
- const memberOwner = workspaceOwnerFor(candidate);
973
- if (!(candidate === node_path.default.posix.join(node_path.default.posix.dirname(file), "package.json")) && !(owner !== void 0 && owner.directory === node_path.default.posix.dirname(file) && memberOwner?.directory === owner.directory && memberOwner.source === owner.source && workspaceContains({
974
- directory: owner.directory,
975
- file: candidate,
976
- patterns: owner.patterns,
977
- rootMembership: "implicit"
978
- }))) continue;
979
- for (const section of [
980
- "dependencies",
981
- "devDependencies",
982
- "optionalDependencies"
983
- ]) {
984
- const entries = member[section];
985
- if (!record$3(entries)) continue;
986
- for (const [name, value] of Object.entries(entries)) {
987
- if (!tsSpecifiers.has(name)) continue;
988
- const values = affectedSpecifiers.get(name) ?? /* @__PURE__ */ new Set();
989
- values.add(resolve(candidate, name, value));
990
- affectedSpecifiers.set(name, values);
991
- }
992
- }
993
- }
994
- const checkResolutionMap = (entries, parent) => {
995
- if (!record$3(entries)) return;
996
- for (const [selector, replacement] of Object.entries(entries)) {
997
- const target = selector === "." ? parent : selector;
998
- if (target === void 0) continue;
999
- let pattern = target.split(/>\s*(?=[@A-Za-z_*]|$)/).at(-1)?.trim() ?? "";
1000
- const version = pattern.lastIndexOf("@");
1001
- let qualifier;
1002
- if (version > pattern.lastIndexOf("/")) {
1003
- qualifier = pattern.slice(version + 1).replace(/^npm:/, "");
1004
- pattern = pattern.slice(0, version);
1005
- }
1006
- const parts = pattern.split("/");
1007
- pattern = parts.at(-2)?.startsWith("@") ? parts.slice(-2).join("/") : parts.at(-1) ?? "";
1008
- if (pattern === "") {
1009
- add({
1010
- file,
1011
- rule: "package-pins",
1012
- key: selector,
1013
- value: replacement,
1014
- message: "resolution selector must name a package"
1015
- });
1016
- continue;
1017
- }
1018
- if (record$3(replacement)) {
1019
- checkResolutionMap(replacement, target);
1020
- continue;
1021
- }
1022
- for (const name of ownedNames) {
1023
- if (!(0, picomatch.default)(pattern)(name)) continue;
1024
- let value = replacement;
1025
- if (typeof replacement === "string" && replacement.startsWith("$")) {
1026
- const reference = replacement.slice(1);
1027
- value = resolutionDependencies[reference];
1028
- }
1029
- const resolved = resolve(file, name, value);
1030
- checkEntries({ [name]: value }, false, {
1031
- key: selector,
1032
- value: replacement
1033
- });
1034
- if (tsSpecifiers.has(name) && [...affectedSpecifiers.get(name) ?? []].some((specifier) => {
1035
- if (resolved === specifier) return false;
1036
- if (qualifier === void 0 || (0, semver.validRange)(qualifier) === null) return true;
1037
- if (typeof specifier !== "string") return false;
1038
- const release = (0, semver.valid)(specifier.startsWith("npm:") ? specifier.slice(specifier.lastIndexOf("@") + 1) : specifier);
1039
- return release !== null && (0, semver.satisfies)(release, qualifier);
1040
- }) && typeof resolved === "string" && tsSpecifiers.get(name)?.has(resolved)) add({
1041
- file,
1042
- rule: "typescript-layout",
1043
- key: selector,
1044
- value: replacement,
1045
- message: `${name} resolution must preserve its declared TypeScript install specifier`
1046
- });
1047
- }
1048
- }
1049
- };
1050
- checkResolutionMap(json["overrides"]);
1051
- checkResolutionMap(json["resolutions"]);
1052
- if (record$3(json["pnpm"])) checkResolutionMap(json["pnpm"]["overrides"]);
1053
- if (policy.typescriptInstallLayouts.some((layout) => dependencies[layout.compilerPackage] !== void 0)) {
1054
- const selectedLayout = policy.typescriptInstallLayouts.find((layout) => {
1055
- if (dependencies[layout.compilerPackage] !== layout.compilerSpecifier) return false;
1056
- if (layout.type === "split-compatibility") return dependencies[layout.compatibilityPackage] === layout.compatibilitySpecifier;
1057
- return policy.typescriptInstallLayouts.every((other) => other.compilerPackage === layout.compilerPackage || dependencies[other.compilerPackage] === void 0);
1058
- });
1059
- if (selectedLayout === void 0) add({
1060
- file,
1061
- rule: "typescript-layout",
1062
- key: "dependencies",
1063
- message: "TypeScript compiler and compatibility packages must match one complete declared install layout"
1064
- });
1065
- const command = record$3(json["scripts"]) ? json["scripts"]["typecheck"] : void 0;
1066
- if (selectedLayout !== void 0 && typeof command === "string") {
1067
- const normalizedExpected = selectedLayout.typecheckCommand.replace(/\s+/g, " ").trim();
1068
- for (const words of compilerCommandSegments(command)) {
1069
- if (!invokesCompiler(words)) continue;
1070
- const normalized = words.join(" ");
1071
- if (!words.some((word) => word === "(" || word === ")") && shellSubstitutions(normalized).length === 0 && (normalized === normalizedExpected || normalized.startsWith(`${normalizedExpected} `))) continue;
1072
- add({
1073
- file,
1074
- rule: "typescript-layout",
1075
- key: "typecheck",
1076
- value: command,
1077
- message: `typecheck compiler invocation must use ${selectedLayout.typecheckCommand}`
1078
- });
1079
- break;
1080
- }
1081
- }
1082
- }
1083
- if (record$3(json["engines"]) && json["engines"]["node"] !== void 0 && (typeof json["engines"]["node"] !== "string" || !nodeSupportRangeMatches(policy.node, json["engines"]["node"]))) add({
1084
- file,
1085
- rule: "node-engine",
1086
- key: "node",
1087
- value: json["engines"]["node"],
1088
- message: `engines.node must support ${policy.node}, found ${JSON.stringify(json["engines"]["node"])}`
1089
- });
1090
- }
1091
- return diagnostics;
1092
- };
1093
- //#endregion
1094
- //#region src/toolchain-runtime.ts
1095
- const runtimeRules = [
1096
- "bun-pins",
1097
- "node-version",
1098
- "python-version",
1099
- "rust-version",
1100
- "runtime-manager",
1101
- "runtime-docker",
1102
- "runtime-workflow",
1103
- "action-pins"
1104
- ];
1105
- const record$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1106
- const pythonSelectorMatches = (value, expected) => value === expected || typeof value === "string" && /^\d+\.\d+$/.test(expected) && value.startsWith(`${expected}.`) && /^\d+\.\d+\.\d+$/.test(value);
1107
- const compareRelease = (left, right) => {
1108
- for (let index = 0; index < Math.max(left.length, right.length); index++) {
1109
- const difference = (left.at(index) ?? 0) - (right.at(index) ?? 0);
1110
- if (difference !== 0) return Math.sign(difference);
1111
- }
1112
- return 0;
1113
- };
1114
- const pythonSeriesRequirementMatches = ({ current, operator, target, wildcard }) => {
1115
- const prefixCovers = (prefix) => prefix.length <= 2 && prefix.every((part, index) => current.at(index) === part);
1116
- const prefixOverlaps = target.slice(0, 2).every((part, index) => current.at(index) === part) && target.slice(3).every((part) => part === 0);
1117
- if (wildcard) {
1118
- if (operator === "==") return prefixCovers(target);
1119
- if (operator === "!=") return !prefixOverlaps;
1120
- return false;
1121
- }
1122
- const minimumComparison = compareRelease(current, target);
1123
- const seriesComparison = compareRelease(current.slice(0, 2), target.slice(0, 2));
1124
- switch (operator) {
1125
- case "==": return false;
1126
- case "!=": return !(seriesComparison === 0 && target.slice(3).every((part) => part === 0));
1127
- case ">=": return minimumComparison >= 0;
1128
- case ">": return minimumComparison > 0;
1129
- case "<=":
1130
- case "<": return seriesComparison < 0;
1131
- case "~=": return target.length >= 2 && minimumComparison >= 0 && prefixCovers(target.slice(0, -1));
1132
- default: return false;
1133
- }
1134
- };
1135
- /** Evaluate final-release Python constraints against the complete selected version set. */
1136
- const pythonRequirementMatches = (requirement, version) => {
1137
- if (typeof requirement !== "string" || requirement.trim() === "") return false;
1138
- const current = version.split(".").map(Number);
1139
- return requirement.split(",").every((clause) => {
1140
- const match = /^(~=|==|!=|<=|>=|<|>)\s*(\d+(?:\.\d+)*)(\.\*)?$/.exec(clause.trim());
1141
- if (match === null) return false;
1142
- const operator = match[1];
1143
- const release = match[2];
1144
- if (release === void 0 || operator === void 0) return false;
1145
- const target = release.split(".").map(Number);
1146
- if (current.length === 2) return pythonSeriesRequirementMatches({
1147
- current,
1148
- operator,
1149
- target,
1150
- wildcard: match[3] !== void 0
1151
- });
1152
- const comparison = compareRelease(current, target);
1153
- if (match[3] !== void 0) {
1154
- if (operator !== "==" && operator !== "!=") return false;
1155
- const prefix = target.every((part, index) => (current.at(index) ?? 0) === part);
1156
- return operator === "==" ? prefix : !prefix;
1157
- }
1158
- switch (operator) {
1159
- case "==": return comparison === 0;
1160
- case "!=": return comparison !== 0;
1161
- case "<=": return comparison <= 0;
1162
- case ">=": return comparison >= 0;
1163
- case "<": return comparison < 0;
1164
- case ">": return comparison > 0;
1165
- case "~=": {
1166
- if (target.length < 2) return false;
1167
- const prefix = target.slice(0, -1);
1168
- return comparison >= 0 && prefix.every((part, index) => (current.at(index) ?? 0) === part);
1169
- }
1170
- default: return false;
1171
- }
1172
- });
1173
- };
1174
- /** Check runtime declarations only in the files that declare or configure them. */
1175
- const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, repository, onDelegated }) => {
1176
- const diagnostics = [];
1177
- const name = node_path.default.posix.basename(file);
1178
- const lines = text.split(/\r?\n/);
1179
- const add = ({ rule, line, message }) => {
1180
- diagnostics.push({
1181
- rule,
1182
- path: file,
1183
- line,
1184
- message
1185
- });
1186
- };
1187
- const lineOf = (key) => Math.max(1, lines.findIndex((line) => line.includes(key)) + 1);
1188
- const pin = ({ rule, value, expected, label, line = 1, selector }) => {
1189
- let matches = value === expected;
1190
- if (selector === "node") matches = nodeSelectorMatches(value, expected);
1191
- else if (label.includes("python")) matches = pythonSelectorMatches(value, expected);
1192
- if (!matches) add({
1193
- rule,
1194
- line,
1195
- message: `${label} must be ${expected}, found ${String(value)}`
1196
- });
1197
- };
1198
- const toml = (rule) => {
1199
- try {
1200
- return (0, smol_toml.parse)(text);
1201
- } catch {
1202
- add({
1203
- rule,
1204
- line: 1,
1205
- message: "invalid TOML"
1206
- });
1207
- return;
1208
- }
1209
- };
1210
- const versions = {
1211
- ...policy.packages,
1212
- ...Object.fromEntries(Object.entries(policy.packages).map(([tool, version]) => [`npm:${tool}`, version])),
1213
- bun: policy.bun,
1214
- node: policy.node,
1215
- nodejs: policy.node,
1216
- python: policy.python,
1217
- rust: policy.rust,
1218
- "core:node": policy.node,
1219
- "core:python": policy.python,
1220
- "core:rust": policy.rust,
1221
- "core:bun": policy.bun
1222
- };
1223
- const nodeTools = /* @__PURE__ */ new Set([
1224
- "node",
1225
- "nodejs",
1226
- "core:node"
1227
- ]);
1228
- if (name === ".bun-version") pin({
1229
- rule: "bun-pins",
1230
- value: text.trim(),
1231
- expected: policy.bun,
1232
- label: name
1233
- });
1234
- if (name === ".node-version" || name === ".nvmrc") pin({
1235
- rule: "node-version",
1236
- selector: "node",
1237
- value: text.trim(),
1238
- expected: policy.node,
1239
- label: name
1240
- });
1241
- if (name === ".python-version") pin({
1242
- rule: "python-version",
1243
- value: text.trim(),
1244
- expected: policy.python,
1245
- label: name
1246
- });
1247
- const firstContent = lines.find((line) => line.trim() !== "" && !line.trimStart().startsWith("#"));
1248
- const rustToml = name === "rust-toolchain.toml" || name === "rust-toolchain" && firstContent?.trimStart().startsWith("[");
1249
- if (name === "rust-toolchain" && !rustToml) pin({
1250
- rule: "rust-version",
1251
- value: text.trim(),
1252
- expected: policy.rust,
1253
- label: name
1254
- });
1255
- if (rustToml) {
1256
- const parsed = toml("rust-version");
1257
- if (parsed !== void 0) {
1258
- const toolchain = parsed["toolchain"];
1259
- const components = record$2(toolchain) ? toolchain["components"] : void 0;
1260
- const expected = Array.isArray(components) && components.includes("rustc-dev") ? policy.rustCompilerDevelopment : policy.rust;
1261
- pin({
1262
- rule: "rust-version",
1263
- value: record$2(toolchain) ? toolchain["channel"] : void 0,
1264
- expected,
1265
- label: "Rust channel",
1266
- line: lineOf("channel")
1267
- });
1268
- }
1269
- }
1270
- if (name === "Cargo.toml") {
1271
- const parsed = toml("rust-version");
1272
- const rustFloor = (value) => {
1273
- const parts = typeof value === "string" && /^(?:0|[1-9]\d*)(?:\.(?:0|[1-9]\d*)){0,2}$/.test(value) ? value.split(".").map(Number) : void 0;
1274
- if (parts === void 0 || !parts.every(Number.isSafeInteger) || compareRelease(parts, policy.rust.split(".").map(Number)) > 0) add({
1275
- rule: "rust-version",
1276
- line: lineOf("rust-version"),
1277
- message: `Cargo rust-version must be a bare support floor at or below Rust ${policy.rust}`
1278
- });
1279
- };
1280
- const readManifest = (candidate) => {
1281
- if (candidate === file) return parsed;
1282
- if (!trackedFiles.has(candidate)) return void 0;
1283
- try {
1284
- return (0, smol_toml.parse)(readFile(candidate) ?? "");
1285
- } catch {
1286
- return;
1287
- }
1288
- };
1289
- if (parsed !== void 0) {
1290
- const workspace = parsed["workspace"];
1291
- const defaults = record$2(workspace) ? workspace["package"] : void 0;
1292
- if (record$2(defaults) && defaults["rust-version"] !== void 0) rustFloor(defaults["rust-version"]);
1293
- const pkg = parsed["package"];
1294
- const version = record$2(pkg) ? pkg["rust-version"] : void 0;
1295
- if (version !== void 0) if (!record$2(version)) rustFloor(version);
1296
- else {
1297
- let inherited;
1298
- if (version["workspace"] === true && record$2(pkg)) {
1299
- const explicit = pkg["workspace"];
1300
- let directory = node_path.default.posix.dirname(file);
1301
- const explicitPath = typeof explicit === "string" && !explicit.includes("\\") && !explicit.includes(":") && !explicit.includes("${{") && !node_path.default.posix.isAbsolute(explicit) ? node_path.default.posix.normalize(node_path.default.posix.join(directory, explicit)) : void 0;
1302
- if (explicit === void 0 || explicitPath !== void 0 && explicitPath !== ".." && !explicitPath.startsWith("../")) {
1303
- if (explicitPath !== void 0) directory = explicitPath;
1304
- while (true) {
1305
- const table = readManifest(node_path.default.posix.join(directory, "Cargo.toml"))?.["workspace"];
1306
- if (record$2(table)) {
1307
- const fields = table["package"];
1308
- inherited = record$2(fields) ? fields["rust-version"] : void 0;
1309
- break;
1310
- }
1311
- if (explicit !== void 0 || directory === ".") break;
1312
- directory = node_path.default.posix.dirname(directory);
1313
- }
1314
- }
1315
- }
1316
- rustFloor(inherited);
1317
- }
1318
- }
1319
- }
1320
- if (name === "Pipfile") {
1321
- const requires = toml("python-version")?.["requires"];
1322
- if (record$2(requires)) {
1323
- const minor = requires["python_version"];
1324
- if (minor !== void 0 && minor !== policy.python.split(".").slice(0, 2).join(".")) add({
1325
- rule: "python-version",
1326
- line: lineOf("python_version"),
1327
- message: `Pipfile python_version must select Python ${policy.python}`
1328
- });
1329
- const full = requires["python_full_version"];
1330
- if (full !== void 0 && !(typeof full === "string" && /^\d+\.\d+\.\d+$/.test(full) && pythonSelectorMatches(full, policy.python))) add({
1331
- rule: "python-version",
1332
- line: lineOf("python_full_version"),
1333
- message: `Pipfile python_full_version must select Python ${policy.python}`
1334
- });
1335
- }
1336
- }
1337
- if (name === "pyproject.toml" || name === "uv.toml") {
1338
- const parsed = toml("python-version");
1339
- if (parsed !== void 0) {
1340
- const project = parsed["project"];
1341
- if (record$2(project) && project["requires-python"] !== void 0) {
1342
- if (!pythonRequirementMatches(project["requires-python"], policy.python)) add({
1343
- rule: "python-version",
1344
- line: lineOf("requires-python"),
1345
- message: `requires-python must include Python ${policy.python}; unsupported constraints must use final-release version specifiers`
1346
- });
1347
- }
1348
- const tool = parsed["tool"];
1349
- const configuredUv = record$2(tool) ? tool["uv"] : void 0;
1350
- const uv = name === "uv.toml" ? parsed : configuredUv;
1351
- if (record$2(uv) && uv["python"] !== void 0) pin({
1352
- rule: "python-version",
1353
- value: uv["python"],
1354
- expected: policy.python,
1355
- label: "uv python",
1356
- line: lineOf("python")
1357
- });
1358
- }
1359
- }
1360
- if (isMiseConfigPath(file)) {
1361
- const tools = toml("runtime-manager")?.["tools"];
1362
- if (record$2(tools)) for (const [tool, expected] of Object.entries(versions)) {
1363
- const configured = tools[tool];
1364
- if (configured !== void 0) pin({
1365
- rule: "runtime-manager",
1366
- value: record$2(configured) ? configured["version"] : configured,
1367
- expected,
1368
- selector: nodeTools.has(tool) ? "node" : void 0,
1369
- label: `mise ${tool}`,
1370
- line: lineOf(tool)
1371
- });
1372
- }
1373
- }
1374
- if (name === ".tool-versions") lines.forEach((line, index) => {
1375
- const fields = line.replace(/#.*$/, "").trim().split(/\s+/);
1376
- const tool = fields.at(0);
1377
- if (tool === void 0 || !(tool in versions)) return;
1378
- const expected = Object.entries(versions).find(([key]) => key === tool)?.[1];
1379
- if (expected !== void 0) pin({
1380
- rule: "runtime-manager",
1381
- value: fields.slice(1).join(" "),
1382
- expected,
1383
- selector: nodeTools.has(tool) ? "node" : void 0,
1384
- label: `.tool-versions ${tool}`,
1385
- line: index + 1
1386
- });
1387
- });
1388
- const checkRuntimeImage = ({ image, line, label }) => {
1389
- if (typeof image !== "string" || image.includes("$")) {
1390
- add({
1391
- rule: "runtime-docker",
1392
- line,
1393
- message: `cannot determine ${label} runtime; use a static image or a reasoned runtime-docker opt-out`
1394
- });
1395
- return;
1396
- }
1397
- const runtime = /^(node|python|oven\/bun)(?::([^@]+))?(?:@.*)?$/i.exec(canonicalDockerRuntime(image));
1398
- if (runtime === null) return;
1399
- const tool = runtime[1]?.toLowerCase();
1400
- if (image.includes("@")) {
1401
- add({
1402
- rule: tool === "oven/bun" ? "bun-pins" : "runtime-docker",
1403
- line,
1404
- message: `${label} ${tool} digest requires an approved digest policy; use a tag-only runtime image`
1405
- });
1406
- return;
1407
- }
1408
- const tag = runtime[2];
1409
- const variantSuffix = tool === "oven/bun" ? /-(?:alpine|slim|debian)$/ : /-(?:alpine(?:\d+(?:\.\d+)*)?|(?:bookworm|bullseye|trixie|buster)(?:-slim)?|slim)$/;
1410
- const version = tag?.replace(variantSuffix, "");
1411
- let expected = policy.bun;
1412
- if (tool === "node") expected = policy.node;
1413
- else if (tool === "python") expected = policy.python;
1414
- pin({
1415
- rule: tool === "oven/bun" ? "bun-pins" : "runtime-docker",
1416
- value: version,
1417
- expected,
1418
- selector: tool === "node" ? "node" : void 0,
1419
- label: `${label} ${tool}`,
1420
- line
1421
- });
1422
- };
1423
- if (isDockerDefinitionPath(file)) {
1424
- const variables = /* @__PURE__ */ new Map();
1425
- const escape = /^\s*#\s*escape\s*=\s*([\\`])\s*$/m.exec(text)?.[1] ?? "\\";
1426
- const continuation = new RegExp(`${escape === "`" ? "`" : "\\\\"}\\s*$`);
1427
- const stages = /* @__PURE__ */ new Set();
1428
- let argumentScope = "global";
1429
- const heredocs = [];
1430
- let instruction = "";
1431
- let startLine = 1;
1432
- const expand = (value) => {
1433
- for (let count = 0; count <= variables.size; count++) {
1434
- const next = value.replace(/\$(?:\{([A-Za-z_][A-Za-z_\d]*)\}|([A-Za-z_][A-Za-z_\d]*))/g, (reference, braced, plain) => variables.get(braced ?? plain ?? "") ?? reference);
1435
- if (next === value) return next;
1436
- value = next;
1437
- }
1438
- return value;
1439
- };
1440
- const collectHeredocs = (content, line) => {
1441
- let quote;
1442
- for (let index = 0; index < content.length; index++) {
1443
- const character = content.at(index);
1444
- if (quote !== void 0) {
1445
- if (character === quote) quote = void 0;
1446
- else if (quote === "\"" && character === "\\") index++;
1447
- continue;
1448
- }
1449
- if (character === "\\") {
1450
- index++;
1451
- continue;
1452
- }
1453
- if (character === "'" || character === "\"") {
1454
- quote = character;
1455
- continue;
1456
- }
1457
- if (character === "#" && (index === 0 || /[\s;&|()]/.test(content.at(index - 1) ?? ""))) break;
1458
- if (character !== "<" || content.at(index + 1) !== "<") continue;
1459
- if (content.at(index + 2) === "<") {
1460
- index += 2;
1461
- continue;
1462
- }
1463
- index += 2;
1464
- const stripTabs = content.at(index) === "-";
1465
- if (stripTabs) index++;
1466
- while (/\s/.test(content.at(index) ?? "")) index++;
1467
- if (content.at(index) === "#") {
1468
- add({
1469
- rule: "runtime-docker",
1470
- line,
1471
- message: "invalid Docker heredoc delimiter"
1472
- });
1473
- break;
1474
- }
1475
- let delimiter = "";
1476
- let delimiterQuote;
1477
- for (; index < content.length; index++) {
1478
- const part = content.at(index);
1479
- if (delimiterQuote !== void 0) {
1480
- if (part === delimiterQuote) delimiterQuote = void 0;
1481
- else if (delimiterQuote === "\"" && part === "\\" && /[$"\\]/.test(content.at(index + 1) ?? "")) delimiter += content.at(++index);
1482
- else delimiter += part;
1483
- continue;
1484
- }
1485
- if (part === "'" || part === "\"") {
1486
- delimiterQuote = part;
1487
- continue;
1488
- }
1489
- if (part === "\\" && content.at(index + 1) !== void 0) {
1490
- delimiter += content.at(++index);
1491
- continue;
1492
- }
1493
- if (/[\s;&|<>()]/.test(part ?? "")) break;
1494
- delimiter += part;
1495
- }
1496
- if (delimiter !== "" && delimiterQuote === void 0) heredocs.push({
1497
- delimiter,
1498
- stripTabs,
1499
- line
1500
- });
1501
- else add({
1502
- rule: "runtime-docker",
1503
- line,
1504
- message: "invalid Docker heredoc delimiter"
1505
- });
1506
- index--;
1507
- }
1508
- };
1509
- const checkInstruction = (content, line) => {
1510
- const argument = /^\s*ARG\s+([A-Za-z_][A-Za-z_\d]*)(?:=(.*))?$/i.exec(content);
1511
- if (argument !== null) {
1512
- if (argumentScope === "stage") return;
1513
- const key = argument[1];
1514
- const value = argument[2];
1515
- if (key !== void 0 && value !== void 0) variables.set(key, expand(value.replace(/^(["'])(.*)\1$/, "$2")));
1516
- return;
1517
- }
1518
- if (/^\s*(?:RUN|COPY)\s+/i.test(content)) {
1519
- collectHeredocs(content, line);
1520
- return;
1521
- }
1522
- const from = /^\s*FROM\s+(?:--platform=\S+\s+)?(\S+)(?:\s+AS\s+(\S+))?/i.exec(content);
1523
- if (from === null || from[1] === void 0) return;
1524
- argumentScope = "stage";
1525
- const image = expand(from[1]);
1526
- if (image.includes("$")) {
1527
- add({
1528
- rule: "runtime-docker",
1529
- line,
1530
- message: "cannot determine FROM runtime; declare an image ARG default or a reasoned runtime-docker opt-out"
1531
- });
1532
- return;
1533
- }
1534
- const stage = from[2]?.toLowerCase();
1535
- const previousStage = stages.has(image.toLowerCase());
1536
- if (stage !== void 0) stages.add(stage);
1537
- if (previousStage) return;
1538
- checkRuntimeImage({
1539
- image,
1540
- line,
1541
- label: "FROM"
1542
- });
1543
- };
1544
- lines.forEach((line, index) => {
1545
- const heredoc = heredocs.at(0);
1546
- if (heredoc !== void 0) {
1547
- if ((heredoc.stripTabs ? line.replace(/^\t+/, "") : line) === heredoc.delimiter) heredocs.shift();
1548
- return;
1549
- }
1550
- if (line.trimStart().startsWith("#")) return;
1551
- if (instruction === "") startLine = index + 1;
1552
- instruction += line.replace(continuation, " ");
1553
- if (continuation.test(line)) return;
1554
- checkInstruction(instruction, startLine);
1555
- instruction = "";
1556
- });
1557
- if (instruction !== "") checkInstruction(instruction, startLine);
1558
- for (const heredoc of heredocs) add({
1559
- rule: "runtime-docker",
1560
- line: heredoc.line,
1561
- message: `unterminated Docker heredoc ${heredoc.delimiter}`
1562
- });
1563
- }
1564
- if (isComposeDefinitionPath(file) || isKubernetesDefinitionPath(file)) {
1565
- const lineCounter = new yaml.LineCounter();
1566
- for (const containerDocument of (0, yaml.parseAllDocuments)(text, {
1567
- lineCounter,
1568
- merge: true,
1569
- uniqueKeys: true
1570
- })) {
1571
- const contents = containerDocument.contents;
1572
- const declaredResource = (0, yaml.isMap)(contents) && (0, yaml.isScalar)(contents.get("apiVersion", true)) && (0, yaml.isScalar)(contents.get("kind", true));
1573
- const recognizedContainer = isComposeDefinitionPath(file) || declaredResource;
1574
- if (containerDocument.errors.length > 0) {
1575
- if (recognizedContainer) add({
1576
- rule: "runtime-docker",
1577
- line: 1,
1578
- message: "invalid container YAML"
1579
- });
1580
- continue;
1581
- }
1582
- try {
1583
- const classified = containerDocumentImages(containerDocument.toJS({ maxAliasCount: 100 }), file);
1584
- for (const entry of classified?.images ?? []) {
1585
- const node = containerDocument.getIn(entry.path, true);
1586
- const line = ((0, yaml.isMap)(node) || (0, yaml.isScalar)(node) || (0, yaml.isSeq)(node) || (0, yaml.isAlias)(node)) && node.range !== void 0 && node.range !== null ? lineCounter.linePos(node.range[0]).line : 1;
1587
- checkRuntimeImage({
1588
- image: entry.image,
1589
- line,
1590
- label: classified?.ecosystem === "docker-compose" ? "Compose image" : "Kubernetes image"
1591
- });
1592
- }
1593
- } catch {
1594
- if (recognizedContainer) add({
1595
- rule: "runtime-docker",
1596
- line: 1,
1597
- message: "invalid container YAML aliases"
1598
- });
1599
- }
1600
- }
1601
- }
1602
- const automationKind = githubAutomationFileKind(file);
1603
- if (automationKind === void 0) return diagnostics;
1604
- const document = (0, yaml.parseDocument)(text, {
1605
- uniqueKeys: true,
1606
- merge: true
1607
- });
1608
- if (document.errors.length > 0) {
1609
- add({
1610
- rule: "runtime-workflow",
1611
- line: 1,
1612
- message: "invalid workflow YAML"
1613
- });
1614
- return diagnostics;
1615
- }
1616
- const nodeLine = (node) => {
1617
- if (!record$2(node) || !Array.isArray(node["range"])) return 1;
1618
- const offset = node["range"].at(0);
1619
- return typeof offset === "number" ? text.slice(0, offset).split("\n").length : 1;
1620
- };
1621
- let checkoutBindings = [];
1622
- let checkoutDestinations = /* @__PURE__ */ new Map();
1623
- let unknownCheckoutDestination = false;
1624
- const staticRepositoryPath = (value) => typeof value === "string" && value !== "" && !value.includes("${{") && !value.includes("\\") && !value.includes(":") && !node_path.default.posix.isAbsolute(value) && !value.split("/").includes("..");
1625
- const normalizeCheckoutPath = (prefix) => node_path.default.posix.normalize(prefix).replace(/\/$/, "") || ".";
1626
- const checkReference = ({ value, tool, line }) => {
1627
- const allowed = {
1628
- node: [".node-version", ".nvmrc"],
1629
- python: [".python-version"],
1630
- bun: ["package.json"]
1631
- }[tool];
1632
- const rule = tool === "bun" ? "bun-pins" : "runtime-workflow";
1633
- const selector = staticRepositoryPath(value) ? node_path.default.posix.normalize(value) : void 0;
1634
- const binding = selector === void 0 ? void 0 : checkoutBindings.findLast((entry) => entry.path === "." || selector === entry.path || selector.startsWith(`${entry.path}/`));
1635
- let target;
1636
- if (!(selector === void 0 || unknownCheckoutDestination || binding?.source === "untrusted" || [...checkoutDestinations].some(([prefix, count]) => count > 1 && (prefix === "." || selector === prefix || selector.startsWith(`${prefix}/`))))) target = binding === void 0 || binding.path === "." ? selector : node_path.default.posix.relative(binding.path, selector);
1637
- if (target === void 0 || !allowed.includes(node_path.default.posix.basename(target)) || binding?.source !== "delegated" && !trackedFiles.has(target)) {
1638
- add({
1639
- rule,
1640
- line,
1641
- message: `setup-${tool} must reference a tracked ${allowed.join(" or ")}`
1642
- });
1643
- return;
1644
- }
1645
- if (binding !== void 0 && (binding.sparse.mode === "invalid" || binding.sparse.mode === "files" && !binding.sparse.paths.has(target))) {
1646
- add({
1647
- rule,
1648
- line,
1649
- message: `checkout sparse-checkout must explicitly list ${target} without dynamic, glob, or negation patterns`
1650
- });
1651
- return;
1652
- }
1653
- if (binding?.source === "delegated" && selector !== void 0) {
1654
- onDelegated?.({
1655
- path: file,
1656
- line,
1657
- tool,
1658
- selector,
1659
- checkoutPath: binding.path,
1660
- ref: binding.ref
1661
- });
1662
- return;
1663
- }
1664
- const content = readFile(target);
1665
- if (tool === "bun") {
1666
- try {
1667
- const manifest = JSON.parse(content ?? "");
1668
- pin({
1669
- rule,
1670
- value: record$2(manifest) ? manifest["packageManager"] : void 0,
1671
- expected: `bun@${policy.bun}`,
1672
- label: "setup-bun version file packageManager",
1673
- line
1674
- });
1675
- } catch {
1676
- add({
1677
- rule,
1678
- line,
1679
- message: "bun-version-file must reference a readable package.json object"
1680
- });
1681
- }
1682
- return;
1683
- }
1684
- pin({
1685
- rule,
1686
- value: content?.trim(),
1687
- expected: policy[tool],
1688
- selector: tool === "node" ? "node" : void 0,
1689
- label: `setup-${tool} version file`,
1690
- line
1691
- });
1692
- };
1693
- const resolveNode = (node) => {
1694
- const visited = /* @__PURE__ */ new Set();
1695
- while ((0, yaml.isAlias)(node)) {
1696
- if (visited.has(node)) throw new Error("cyclic YAML alias");
1697
- visited.add(node);
1698
- node = node.resolve(document);
1699
- }
1700
- return node;
1701
- };
1702
- const approvedActions = new Map(Object.entries(policy.actions).map(([action, approved]) => [action.toLowerCase(), approved]));
1703
- const mergedMaps = (node) => {
1704
- node = resolveNode(node);
1705
- if (!(0, yaml.isMap)(node)) return [];
1706
- const maps = [];
1707
- for (const pair of node.items) {
1708
- if (!(0, yaml.isScalar)(pair.key) || typeof pair.key.value !== "symbol" || pair.key.source !== "<<") continue;
1709
- const merge = resolveNode(pair.value);
1710
- maps.push(...(0, yaml.isSeq)(merge) ? merge.items : [merge]);
1711
- }
1712
- return maps;
1713
- };
1714
- const keysOf = (node, visited = /* @__PURE__ */ new Set()) => {
1715
- node = resolveNode(node);
1716
- if (!(0, yaml.isMap)(node)) return /* @__PURE__ */ new Set();
1717
- if (visited.has(node)) throw new Error("cyclic YAML merge");
1718
- const next = new Set(visited);
1719
- next.add(node);
1720
- const keys = new Set(node.items.flatMap((pair) => (0, yaml.isScalar)(pair.key) && typeof pair.key.value === "string" ? [pair.key.value] : []));
1721
- for (const merged of mergedMaps(node)) for (const key of keysOf(merged, next)) keys.add(key);
1722
- return keys;
1723
- };
1724
- const sourceMap = ({ node, key, visited = /* @__PURE__ */ new Set() }) => {
1725
- node = resolveNode(node);
1726
- if (!(0, yaml.isMap)(node)) return void 0;
1727
- if (node.has(key)) return node;
1728
- if (visited.has(node)) throw new Error("cyclic YAML merge");
1729
- const next = new Set(visited);
1730
- next.add(node);
1731
- for (const merged of mergedMaps(node)) {
1732
- const source = sourceMap({
1733
- node: merged,
1734
- key,
1735
- visited: next
1736
- });
1737
- if (source !== void 0) return source;
1738
- }
1739
- };
1740
- const getNode = (node, key) => {
1741
- const source = sourceMap({
1742
- node,
1743
- key
1744
- });
1745
- return (0, yaml.isMap)(source) ? resolveNode(source.get(key, true)) : void 0;
1746
- };
1747
- const actionInputs = /* @__PURE__ */ new Map();
1748
- const inputsOf = (node) => {
1749
- node = resolveNode(node);
1750
- const cached = actionInputs.get(node);
1751
- if (cached !== void 0) return cached;
1752
- const options = getNode(node, "with");
1753
- const inputs = /* @__PURE__ */ new Map();
1754
- for (const key of keysOf(options)) {
1755
- const folded = key.toUpperCase();
1756
- const value = getNode(options, key);
1757
- if (inputs.has(folded)) add({
1758
- rule: "action-pins",
1759
- line: nodeLine(value),
1760
- message: `action input ${key} collides after case-folding`
1761
- });
1762
- if (/(?:^|-)VERSION(?:-FILE)?$/.test(folded) && key !== folded.toLowerCase()) add({
1763
- rule: "action-pins",
1764
- line: nodeLine(value),
1765
- message: `selector input ${key} must use canonical lowercase casing`
1766
- });
1767
- inputs.set(folded, value);
1768
- }
1769
- actionInputs.set(node, inputs);
1770
- return inputs;
1771
- };
1772
- const getInput = (node, key) => inputsOf(node).get(key.toUpperCase());
1773
- const dynamicRefBody = (value) => {
1774
- if (typeof value !== "string" || !value.startsWith("${{")) return void 0;
1775
- let quote;
1776
- for (let index = 3; index < value.length; index++) {
1777
- const character = value.at(index);
1778
- if (quote !== void 0) {
1779
- if (character === quote) if (quote === "'" && value.at(index + 1) === "'") index++;
1780
- else quote = void 0;
1781
- else if (quote === "\"" && character === "\\") index++;
1782
- continue;
1783
- }
1784
- if (character === "'" || character === "\"") quote = character;
1785
- else if (value.startsWith("${{", index)) return void 0;
1786
- else if (character === "}" && value.at(index + 1) === "}") return index === value.length - 2 ? value.slice(3, index).trim() : void 0;
1787
- }
1788
- };
1789
- const checkoutPrefixOf = (node) => {
1790
- const destination = getInput(node, "path");
1791
- if (destination === void 0) return ".";
1792
- return (0, yaml.isScalar)(destination) ? destination.value : void 0;
1793
- };
1794
- const sparseCheckoutOf = (node) => {
1795
- const sparseKeys = [...inputsOf(node).keys()].filter((key) => key.startsWith("SPARSE-CHECKOUT"));
1796
- if (sparseKeys.length === 0) return { mode: "all" };
1797
- if (sparseKeys.some((key) => key !== "SPARSE-CHECKOUT" && key !== "SPARSE-CHECKOUT-CONE-MODE")) return { mode: "invalid" };
1798
- const value = getInput(node, "sparse-checkout");
1799
- const cone = getInput(node, "sparse-checkout-cone-mode");
1800
- if (!(0, yaml.isScalar)(value) || typeof value.value !== "string" || cone !== void 0 && (!(0, yaml.isScalar)(cone) || cone.value !== true && cone.value !== false && cone.value !== "true" && cone.value !== "false")) return { mode: "invalid" };
1801
- const paths = value.value.split(/\r?\n/).map((entry) => entry.trim()).filter((entry) => entry !== "");
1802
- if (paths.length === 0 || paths.some((entry) => !staticRepositoryPath(entry) || entry.startsWith("!") || /[*?[\]]/.test(entry))) return { mode: "invalid" };
1803
- return {
1804
- mode: "files",
1805
- paths: new Set(paths)
1806
- };
1807
- };
1808
- const checkAction = (node) => {
1809
- node = resolveNode(node);
1810
- if (!(0, yaml.isMap)(node)) return;
1811
- inputsOf(node);
1812
- const uses = getNode(node, "uses");
1813
- if (uses === void 0) return;
1814
- const line = nodeLine(uses);
1815
- if (!(0, yaml.isScalar)(uses) || typeof uses.value !== "string") {
1816
- add({
1817
- rule: "action-pins",
1818
- line,
1819
- message: "action uses must be a string containing a full commit SHA"
1820
- });
1821
- return;
1822
- }
1823
- const value = uses.value;
1824
- if (value.startsWith("$/")) {
1825
- if (value.length === 2 || /\s/.test(value) || value.includes("@") || value.includes("${{") || value.includes("\\") || value.split("/").includes("..")) add({
1826
- rule: "action-pins",
1827
- line,
1828
- message: "self-repository actions must use a static repository path without a ref suffix"
1829
- });
1830
- return;
1831
- }
1832
- if (value.startsWith("./")) {
1833
- const selector = staticRepositoryPath(value) ? node_path.default.posix.normalize(value) : void 0;
1834
- const binding = selector === void 0 ? void 0 : checkoutBindings.findLast((entry) => entry.path === "." || selector === entry.path || selector.startsWith(`${entry.path}/`));
1835
- if (selector === void 0 || unknownCheckoutDestination || binding !== void 0 && binding.source !== "tracked" || [...checkoutDestinations].some(([prefix, count]) => count > 1 && (prefix === "." || selector === prefix || selector.startsWith(`${prefix}/`)))) add({
1836
- rule: "action-pins",
1837
- line,
1838
- message: "local actions require an unambiguous checkout of the current source snapshot"
1839
- });
1840
- return;
1841
- }
1842
- if (value.startsWith("docker://")) {
1843
- add({
1844
- rule: "action-pins",
1845
- line,
1846
- message: "Docker image actions require a reasoned action-pins opt-out"
1847
- });
1848
- checkRuntimeImage({
1849
- image: value.slice(9),
1850
- line,
1851
- label: "Docker action image"
1852
- });
1853
- return;
1854
- }
1855
- const remote = /^([^./][^\s@]*\/[^\s@]+)@([^\s]+)$/.exec(value);
1856
- if (remote === null) {
1857
- add({
1858
- rule: "action-pins",
1859
- line,
1860
- message: "remote actions must use a full commit SHA"
1861
- });
1862
- return;
1863
- }
1864
- const action = remote[1]?.toLowerCase();
1865
- const ref = remote[2];
1866
- const approved = action === void 0 ? void 0 : approvedActions.get(action) ?? approvedActions.get(action.split("/").slice(0, 2).join("/"));
1867
- if (ref === void 0 || !/^[a-f0-9]{40}$/.test(ref)) add({
1868
- rule: "action-pins",
1869
- line,
1870
- message: "remote actions must use a full commit SHA"
1871
- });
1872
- else if (approved !== void 0 && ref !== approved.sha) add({
1873
- rule: "action-pins",
1874
- line,
1875
- message: `${action} must use ${approved.sha}`
1876
- });
1877
- const declaration = sourceMap({
1878
- node,
1879
- key: "uses"
1880
- });
1881
- const comment = uses.comment ?? ((0, yaml.isMap)(declaration) ? declaration.comment : void 0) ?? "";
1882
- if (approved !== void 0 && !comment.trim().split(/\s+/).includes(approved.version)) add({
1883
- rule: "action-pins",
1884
- line,
1885
- message: `${action} requires a # ${approved.version} version comment`
1886
- });
1887
- if (action === "actions/checkout") {
1888
- const checkoutRepository = getInput(node, "repository");
1889
- const checkoutRef = getInput(node, "ref");
1890
- const prefix = checkoutPrefixOf(node);
1891
- let repo;
1892
- if (checkoutRepository !== void 0) repo = (0, yaml.isScalar)(checkoutRepository) ? checkoutRepository.value : null;
1893
- const self = repo === void 0 || repo === "${{ github.repository }}" || typeof repo === "string" && repository !== void 0 && repo.toLowerCase() === repository.toLowerCase();
1894
- const workflowSource = repo === "${{ job.workflow_repository }}" && (0, yaml.isScalar)(checkoutRef) && checkoutRef.value === "${{ job.workflow_sha }}";
1895
- const currentSource = self && checkoutRef === void 0;
1896
- const refValue = (0, yaml.isScalar)(checkoutRef) ? checkoutRef.value : void 0;
1897
- const dynamicRef = dynamicRefBody(refValue);
1898
- const delegatedSource = (self || repo === "${{ job.workflow_repository }}") && dynamicRef !== void 0 && dynamicRef !== "" && !/^(?:'(?:[^']|'')*'|"(?:[^"\\]|\\.)*"|(?:true|false|null)|-?(?:0x[\da-f]+|(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?))$/i.test(dynamicRef) && !/^(?:github\s*(?:\.\s*sha|\[\s*['"]sha['"]\s*\])|job\s*(?:\.\s*workflow_sha|\[\s*['"]workflow_sha['"]\s*\]))$/i.test(dynamicRef);
1899
- if (!staticRepositoryPath(prefix)) checkoutBindings.push({
1900
- path: ".",
1901
- source: "untrusted",
1902
- sparse: { mode: "invalid" }
1903
- });
1904
- else {
1905
- const destination = normalizeCheckoutPath(prefix);
1906
- const trustedWriter = ref === approved?.sha && sourceMap({
1907
- node,
1908
- key: "if"
1909
- }) === void 0 && sourceMap({
1910
- node,
1911
- key: "continue-on-error"
1912
- }) === void 0 && checkoutDestinations.get(destination) === 1 && !unknownCheckoutDestination;
1913
- if (trustedWriter && delegatedSource && typeof refValue === "string") checkoutBindings.push({
1914
- path: destination,
1915
- sparse: sparseCheckoutOf(node),
1916
- source: "delegated",
1917
- ref: refValue
1918
- });
1919
- else checkoutBindings.push({
1920
- path: destination,
1921
- sparse: sparseCheckoutOf(node),
1922
- source: trustedWriter && (workflowSource || currentSource) ? "tracked" : "untrusted"
1923
- });
1924
- }
1925
- }
1926
- let tool;
1927
- switch (action) {
1928
- case "actions/setup-node":
1929
- tool = "node";
1930
- break;
1931
- case "actions/setup-python":
1932
- tool = "python";
1933
- break;
1934
- case "oven-sh/setup-bun":
1935
- tool = "bun";
1936
- break;
1937
- default: return;
1938
- }
1939
- const literal = getInput(node, `${tool}-version`);
1940
- if (literal !== void 0) add({
1941
- rule: tool === "bun" ? "bun-pins" : "runtime-workflow",
1942
- line: nodeLine(literal),
1943
- message: `setup-${tool} must use ${tool}-version-file`
1944
- });
1945
- const reference = getInput(node, `${tool}-version-file`);
1946
- checkReference({
1947
- value: (0, yaml.isScalar)(reference) ? reference.value : void 0,
1948
- tool,
1949
- line: reference === void 0 ? line : nodeLine(reference)
1950
- });
1951
- };
1952
- const checkContainer = (node) => {
1953
- node = resolveNode(node);
1954
- if (node === void 0) return;
1955
- const image = (0, yaml.isMap)(node) ? getNode(node, "image") : node;
1956
- checkRuntimeImage({
1957
- image: (0, yaml.isScalar)(image) ? image.value : void 0,
1958
- line: nodeLine(image ?? node),
1959
- label: "container image"
1960
- });
1961
- };
1962
- const checkSteps = (node) => {
1963
- checkoutBindings = [];
1964
- checkoutDestinations = /* @__PURE__ */ new Map();
1965
- unknownCheckoutDestination = false;
1966
- node = resolveNode(node);
1967
- if (!(0, yaml.isSeq)(node)) return;
1968
- for (const step of node.items) {
1969
- const uses = getNode(step, "uses");
1970
- if (!(0, yaml.isScalar)(uses) || typeof uses.value !== "string" || !uses.value.toLowerCase().startsWith("actions/checkout@")) continue;
1971
- const prefix = checkoutPrefixOf(step);
1972
- if (!staticRepositoryPath(prefix)) {
1973
- unknownCheckoutDestination = true;
1974
- continue;
1975
- }
1976
- const destination = normalizeCheckoutPath(prefix);
1977
- checkoutDestinations.set(destination, (checkoutDestinations.get(destination) ?? 0) + 1);
1978
- }
1979
- for (const step of node.items) checkAction(step);
1980
- };
1981
- try {
1982
- document.toJS({ maxAliasCount: 100 });
1983
- if (automationKind === "workflow") {
1984
- const jobs = getNode(document.contents, "jobs");
1985
- if ((0, yaml.isMap)(jobs)) for (const key of keysOf(jobs)) {
1986
- checkoutBindings = [];
1987
- checkoutDestinations = /* @__PURE__ */ new Map();
1988
- unknownCheckoutDestination = false;
1989
- const job = getNode(jobs, key);
1990
- checkAction(job);
1991
- checkContainer(getNode(job, "container"));
1992
- const services = getNode(job, "services");
1993
- if ((0, yaml.isMap)(services)) for (const service of keysOf(services)) checkContainer(getNode(services, service));
1994
- checkSteps(getNode(job, "steps"));
1995
- }
1996
- } else {
1997
- const runs = getNode(document.contents, "runs");
1998
- const using = getNode(runs, "using");
1999
- if ((0, yaml.isScalar)(using) && using.value === "composite") checkSteps(getNode(runs, "steps"));
2000
- if ((0, yaml.isScalar)(using) && using.value === "docker") {
2001
- const image = getNode(runs, "image");
2002
- const value = (0, yaml.isScalar)(image) ? image.value : void 0;
2003
- if (typeof value === "string" && value.startsWith("docker://")) {
2004
- add({
2005
- rule: "action-pins",
2006
- line: nodeLine(image),
2007
- message: "Docker image actions require a reasoned action-pins opt-out"
2008
- });
2009
- checkRuntimeImage({
2010
- image: value.slice(9),
2011
- line: nodeLine(image),
2012
- label: "Docker action image"
2013
- });
2014
- } else if (typeof value !== "string" || value.includes("${{") || value.includes("\\") || value.includes(":") || node_path.default.posix.isAbsolute(value) || node_path.default.posix.normalize(node_path.default.posix.join(node_path.default.posix.dirname(file), value)).split("/").at(0) === ".." || node_path.default.posix.basename(value) !== "Dockerfile") add({
2015
- rule: "action-pins",
2016
- line: nodeLine(image),
2017
- message: "Docker actions must reference a local Dockerfile or declare a reasoned action-pins opt-out for an external image"
2018
- });
2019
- }
2020
- }
2021
- } catch {
2022
- add({
2023
- rule: "runtime-workflow",
2024
- line: 1,
2025
- message: "cannot resolve workflow YAML aliases"
2026
- });
2027
- }
2028
- return diagnostics;
2029
- };
2030
- //#endregion
2031
- //#region src/toolchain-schema.ts
2032
- const packagePinKeys = [
2033
- "oxlint",
2034
- "oxlint-tsgolint",
2035
- "@oxlint/plugins",
2036
- "@stll/oxlint-plugin",
2037
- "oxfmt",
2038
- "lefthook"
2039
- ];
2040
- const record$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2041
- /** Reject unsupported or incomplete installed policies before inspecting a consumer. */
2042
- const parseToolchainPolicy = (input) => {
2043
- if (!record$1(input) || input["schemaVersion"] !== 1) throw new Error("toolchain.json must use schemaVersion 1");
2044
- const string = (object, key) => {
2045
- const value = object[key];
2046
- if (typeof value !== "string" || value.trim() === "") throw new Error(`toolchain.json requires a nonempty ${key}`);
2047
- return value;
2048
- };
2049
- const packages = {};
2050
- for (const key of packagePinKeys) packages[key] = string(input, key);
2051
- const bun = string(input, "bun");
2052
- const typescript = string(input, "typescript");
2053
- const node = string(input, "node");
2054
- const python = string(input, "python");
2055
- const rust = string(input, "rust");
2056
- const rustCompilerDevelopment = string(input, "rustCompilerDevelopment");
2057
- for (const [name, value] of Object.entries({
2058
- ...packages,
2059
- bun,
2060
- typescript
2061
- })) if ((0, semver.valid)(value) !== value) throw new Error(`toolchain.json ${name} must be an exact release`);
2062
- if (!nodePolicyValid(node)) throw new Error("Node must be a canonical major N.x series");
2063
- if (!/^\d+\.\d+(?:\.\d+)?$/.test(python)) throw new Error("Python must be an exact minor or patch release");
2064
- if (!/^\d+\.\d+\.\d+$/.test(rust)) throw new Error("Rust must be an exact stable release");
2065
- if (!/^nightly-\d{4}-\d{2}-\d{2}$/.test(rustCompilerDevelopment)) throw new Error("Rust compiler development must be a dated nightly");
2066
- const compatibility = input["typescript6Compatibility"];
2067
- if (!record$1(compatibility)) throw new Error("missing TypeScript compatibility policy");
2068
- const version = string(compatibility, "version");
2069
- const packageAlias = string(compatibility, "packageAlias");
2070
- if ((0, semver.valid)(version) !== version) throw new Error("TypeScript compatibility must be an exact release");
2071
- const layouts = input["typescriptInstallLayouts"];
2072
- if (!Array.isArray(layouts) || layouts.length === 0) throw new Error("missing TypeScript install layouts");
2073
- const typescriptInstallLayouts = layouts.map((layout) => {
2074
- if (!record$1(layout)) throw new Error("invalid TypeScript install layout");
2075
- const compilerPackage = string(layout, "compilerPackage");
2076
- const compilerSpecifier = string(layout, "compilerSpecifier");
2077
- const typecheckCommand = string(layout, "typecheckCommand");
2078
- switch (layout["type"]) {
2079
- case "direct":
2080
- if (compilerSpecifier !== typescript || compilerPackage !== "typescript") throw new Error("direct TypeScript layout differs from shared version");
2081
- return {
2082
- type: "direct",
2083
- compilerPackage,
2084
- compilerSpecifier,
2085
- typecheckCommand
2086
- };
2087
- case "split-compatibility": {
2088
- const compatibilityPackage = string(layout, "compatibilityPackage");
2089
- const compatibilitySpecifier = string(layout, "compatibilitySpecifier");
2090
- if (compilerSpecifier !== `npm:typescript@${typescript}` || compatibilitySpecifier !== version) throw new Error("split TypeScript layout differs from shared versions");
2091
- return {
2092
- type: "split-compatibility",
2093
- compilerPackage,
2094
- compilerSpecifier,
2095
- compatibilityPackage,
2096
- compatibilitySpecifier,
2097
- typecheckCommand
2098
- };
2099
- }
2100
- default: throw new Error("unsupported TypeScript install layout");
2101
- }
2102
- });
2103
- const actionsInput = input["actions"];
2104
- if (!record$1(actionsInput) || Object.keys(actionsInput).length === 0) throw new Error("missing approved actions");
2105
- const actions = {};
2106
- for (const [name, action] of Object.entries(actionsInput)) {
2107
- if (!record$1(action)) throw new Error(`invalid action policy: ${name}`);
2108
- const sha = string(action, "sha");
2109
- const actionVersion = string(action, "version");
2110
- if (!/^[a-f0-9]{40}$/.test(sha) || !/^v\d+(?:\.\d+)*$/.test(actionVersion)) throw new Error(`invalid action SHA/version: ${name}`);
2111
- actions[name] = {
2112
- sha,
2113
- version: actionVersion
2114
- };
2115
- }
2116
- const dependabotInput = input["dependabot"];
2117
- if (!record$1(dependabotInput) || !record$1(dependabotInput["schedule"]) || !record$1(dependabotInput["cooldown"]) || !record$1(dependabotInput["groups"])) throw new Error("missing Dependabot policy");
2118
- const scheduleInput = dependabotInput["schedule"];
2119
- const schedule = {
2120
- interval: string(scheduleInput, "interval"),
2121
- day: string(scheduleInput, "day"),
2122
- time: string(scheduleInput, "time"),
2123
- timezone: string(scheduleInput, "timezone")
2124
- };
2125
- const defaultDays = dependabotInput["cooldown"]["defaultDays"];
2126
- if (typeof defaultDays !== "number" || !Number.isInteger(defaultDays) || defaultDays < 5) throw new Error("Dependabot cooldown must be at least five days");
2127
- const strings = (value) => {
2128
- if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) throw new Error("Dependabot group values must be string arrays");
2129
- return value;
2130
- };
2131
- const groups = {};
2132
- for (const [name, group] of Object.entries(dependabotInput["groups"])) {
2133
- if (!record$1(group)) throw new Error(`invalid Dependabot group: ${name}`);
2134
- groups[name] = {
2135
- patterns: strings(group["patterns"]),
2136
- updateTypes: strings(group["updateTypes"])
2137
- };
2138
- }
2139
- return {
2140
- bun,
2141
- typescript,
2142
- node,
2143
- python,
2144
- rust,
2145
- rustCompilerDevelopment,
2146
- packages,
2147
- actions,
2148
- typescriptInstallLayouts,
2149
- typescript6Compatibility: {
2150
- version,
2151
- packageAlias
2152
- },
2153
- dependabot: {
2154
- schedule,
2155
- cooldown: { defaultDays },
2156
- groups,
2157
- ignoredPackages: [.../* @__PURE__ */ new Set([
2158
- ...Object.keys(packages),
2159
- ...typescriptInstallLayouts.flatMap((layout) => layout.type === "direct" ? [layout.compilerPackage] : [layout.compilerPackage, layout.compatibilityPackage]),
2160
- packageAlias,
2161
- "bun-types"
2162
- ])],
2163
- ignoredActions: Object.keys(actions),
2164
- ignoredImages: [
2165
- "node",
2166
- "python",
2167
- "oven/bun"
2168
- ]
2169
- }
2170
- };
2171
- };
2172
- //#endregion
2173
- //#region src/toolchain-guard.ts
2174
- const toolchainRules = [.../* @__PURE__ */ new Set([
2175
- ...packageRules,
2176
- ...runtimeRules,
2177
- ...dependabotRules
2178
- ])];
2179
- const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2180
- const array = (value) => Array.isArray(value);
2181
- /** Opt-outs are repository-owned, tracked decisions; malformed decisions fail closed. */
2182
- const parseToolchainOptOuts = (input) => {
2183
- if (!record(input) || Object.keys(input).some((key) => key !== "optOuts") || !array(input["optOuts"])) throw new Error("stll-toolchain.json must contain only an \"optOuts\" array");
2184
- const disabled = /* @__PURE__ */ new Set();
2185
- for (const entry of input["optOuts"]) {
2186
- if (!record(entry) || Object.keys(entry).some((key) => key !== "rule" && key !== "reason") || typeof entry["rule"] !== "string" || !toolchainRules.some((rule) => rule === entry["rule"]) || typeof entry["reason"] !== "string" || entry["reason"].trim() === "" || disabled.has(entry["rule"])) throw new Error("each opt-out requires a unique known rule and a nonempty reason");
2187
- if (entry["rule"] === "node-engine") throw new Error("node-engine cannot be opted out; engines.node must support the shared Node series");
2188
- disabled.add(entry["rule"]);
2189
- }
2190
- return disabled;
2191
- };
2192
- /** Read one tracked configuration snapshot for guards and policy generation. */
2193
- const readToolchainInputs = (root) => {
2194
- const diagnostics = [];
2195
- const tracked = (0, node_child_process.execFileSync)("git", [
2196
- "ls-files",
2197
- "-z",
2198
- "--cached"
2199
- ], {
2200
- cwd: root,
2201
- encoding: "utf8"
2202
- }).split("\0").filter((file) => file !== "");
2203
- const files = {};
2204
- const trackedFiles = new Set(tracked);
2205
- const resolvedRoot = (0, node_fs.realpathSync)(root);
2206
- for (const file of tracked) {
2207
- const kind = toolchainInputKind(file);
2208
- if (kind === void 0) continue;
2209
- if (kind === "presence") {
2210
- files[file] = "";
2211
- continue;
2212
- }
2213
- try {
2214
- const resolvedFile = (0, node_fs.realpathSync)(node_path.default.join(resolvedRoot, file));
2215
- const relative = node_path.default.relative(resolvedRoot, resolvedFile);
2216
- if (node_path.default.isAbsolute(relative) || relative.split(node_path.default.sep).at(0) === ".." || !trackedFiles.has(relative.split(node_path.default.sep).join("/"))) throw new Error("Configuration target is outside the tracked tree");
2217
- files[file] = (0, node_fs.readFileSync)(resolvedFile, "utf8");
2218
- } catch {
2219
- diagnostics.push({
2220
- rule: "configuration",
2221
- path: file,
2222
- line: 1,
2223
- message: "cannot read tracked configuration file"
2224
- });
2225
- }
2226
- }
2227
- return {
2228
- files,
2229
- trackedFiles,
2230
- diagnostics
2231
- };
2232
- };
2233
- const checkToolchain = ({ root, policy, onDelegated }) => {
2234
- const { files, trackedFiles, diagnostics } = readToolchainInputs(root);
2235
- let repository;
2236
- try {
2237
- const origin = (0, node_child_process.execFileSync)("git", [
2238
- "remote",
2239
- "get-url",
2240
- "origin"
2241
- ], {
2242
- cwd: root,
2243
- encoding: "utf8",
2244
- stdio: [
2245
- "ignore",
2246
- "pipe",
2247
- "ignore"
2248
- ]
2249
- }).trim();
2250
- repository = /^(?:https?:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([^/]+\/[^/]+?)(?:\.git)?\/?$/.exec(origin)?.[1];
2251
- } catch {}
2252
- let disabled = /* @__PURE__ */ new Set();
2253
- if (files["stll-toolchain.json"] !== void 0) try {
2254
- disabled = parseToolchainOptOuts(JSON.parse(files["stll-toolchain.json"]));
2255
- } catch (error) {
2256
- diagnostics.push({
2257
- rule: "configuration",
2258
- path: "stll-toolchain.json",
2259
- line: 1,
2260
- message: error instanceof Error ? error.message : String(error)
2261
- });
2262
- }
2263
- diagnostics.push(...checkPackageFiles({
2264
- files,
2265
- policy
2266
- }));
2267
- for (const [file, text] of Object.entries(files)) diagnostics.push(...checkRuntimeFile({
2268
- file,
2269
- text,
2270
- policy: {
2271
- ...policy,
2272
- packages: {
2273
- ...policy.packages,
2274
- typescript: policy.typescript
2275
- }
2276
- },
2277
- trackedFiles,
2278
- repository,
2279
- onDelegated,
2280
- readFile: (target) => files[target]
2281
- }));
2282
- diagnostics.push(...checkDependabot({
2283
- files,
2284
- policy: policy.dependabot
2285
- }));
2286
- return diagnostics.filter((diagnostic) => !disabled.has(diagnostic.rule));
2287
- };
2288
- //#endregion
2289
5
  //#region src/toolchain-check-cli.ts
2290
6
  try {
2291
7
  const args = process.argv.slice(2);
@@ -2294,8 +10,8 @@ try {
2294
10
  process.exit(0);
2295
11
  }
2296
12
  if (args.length !== 0) throw new Error(`unexpected argument: ${args.at(0)}`);
2297
- const policy = parseToolchainPolicy(JSON.parse((0, node_fs.readFileSync)(new URL("../toolchain.json", require("url").pathToFileURL(__filename).href), "utf8")));
2298
- const diagnostics = checkToolchain({
13
+ const policy = require_toolchain_guard.parseToolchainPolicy(JSON.parse((0, node_fs.readFileSync)(new URL("../toolchain.json", require("url").pathToFileURL(__filename).href), "utf8")));
14
+ const diagnostics = require_toolchain_guard.checkToolchain({
2299
15
  root: process.cwd(),
2300
16
  policy,
2301
17
  onDelegated: ({ path, line, tool, selector, ref }) => process.stdout.write(`${path}:${line}: [runtime-delegated] setup-${tool} ${selector} delegates its version to checkout ref ${ref}\n`)