@stll/oxlint-config 0.12.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -4
- package/dist/toolchain-guard.cjs +84 -41
- package/dist/toolchain-guard.cjs.map +1 -1
- package/dist/toolchain-guard.mjs +84 -41
- package/dist/toolchain-guard.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,6 +92,15 @@ part of major 26 fail.
|
|
|
92
92
|
Release and publish workflows use exact patch version-file selectors for
|
|
93
93
|
reproducible builds; this repository pins `.node-version` to `26.10.0`.
|
|
94
94
|
The series policy accepts that patch without weakening release validation.
|
|
95
|
+
Runtime images may retain a full SHA256 digest alongside an exact stable patch
|
|
96
|
+
tag: Node must be inside its policy series and match tracked root `.node-version`
|
|
97
|
+
when present; Python must be a patch in its policy series; Bun must equal its
|
|
98
|
+
policy version. Supported image variants remain allowed.
|
|
99
|
+
Digest validation is declaration-only: the guard enforces the exact policy tag
|
|
100
|
+
and a full SHA256 digest, but does not verify that the digest belongs to the tag.
|
|
101
|
+
Dependabot refreshes digests per repository; a shared tag-to-digest map would
|
|
102
|
+
require a tooling release for every base-image digest bump across repositories,
|
|
103
|
+
which costs more than it buys.
|
|
95
104
|
GitHub JavaScript action host runtimes (`runs.using`) are platform-managed
|
|
96
105
|
and do not select the project toolchain Node version.
|
|
97
106
|
|
|
@@ -198,10 +207,11 @@ Add an explicit declaration to tracked root `stll-toolchain.json`:
|
|
|
198
207
|
}
|
|
199
208
|
```
|
|
200
209
|
|
|
201
|
-
Services are optional
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
210
|
+
Services are optional: omit `services` or use `[]` for runtimes and dependencies
|
|
211
|
+
only. No `cloud` declaration means no cloud setup script. `envFile` is required
|
|
212
|
+
with services and rejected without services; runtime-only scripts emit no
|
|
213
|
+
environment file code. With services, `envFile` must be a canonical
|
|
214
|
+
repository-relative path; its existing parent must be safe at runtime. Track an exact stable Node patch in root
|
|
205
215
|
`.node-version` within the shared Node series. Generate and commit the script:
|
|
206
216
|
|
|
207
217
|
```sh
|
package/dist/toolchain-guard.cjs
CHANGED
|
@@ -291,19 +291,28 @@ const parseCloudSetup = (input) => {
|
|
|
291
291
|
if (input === void 0) return void 0;
|
|
292
292
|
if (typeof input !== "object" || input === null || Array.isArray(input)) throw new Error("cloud must be an object");
|
|
293
293
|
for (const key of Object.keys(input)) if (key !== "services" && key !== "install" && key !== "envFile") throw new Error(`unsupported cloud key: ${key}`);
|
|
294
|
-
|
|
294
|
+
const selected = "services" in input ? input.services : [];
|
|
295
|
+
if (!Array.isArray(selected)) throw new Error("cloud.services must be an array when provided");
|
|
295
296
|
if (!("install" in input) || input.install !== "bun install --frozen-lockfile") throw new Error(`cloud.install must be exactly ${cloudInstallCommand}`);
|
|
297
|
+
const seen = /* @__PURE__ */ new Set();
|
|
298
|
+
const services = Array.from(selected, (service) => {
|
|
299
|
+
if (!cloudService(service)) throw new Error("cloud.services contains an unsupported service");
|
|
300
|
+
if (seen.has(service)) throw new Error(`cloud.services contains duplicate service: ${service}`);
|
|
301
|
+
seen.add(service);
|
|
302
|
+
return service;
|
|
303
|
+
});
|
|
304
|
+
if (services.length === 0) {
|
|
305
|
+
if ("envFile" in input) throw new Error("cloud.envFile is not allowed without services");
|
|
306
|
+
return {
|
|
307
|
+
services,
|
|
308
|
+
install: cloudInstallCommand
|
|
309
|
+
};
|
|
310
|
+
}
|
|
296
311
|
if (!("envFile" in input) || typeof input.envFile !== "string") throw new Error("cloud.envFile must be a canonical repository-relative path");
|
|
297
312
|
const envFile = input.envFile;
|
|
298
313
|
if (envFile.startsWith("/") || /^[A-Za-z]:/.test(envFile) || envFile.includes("\\") || /\p{Cc}/u.test(envFile) || envFile.split("/").some((part) => part === "" || part === "." || part === "..")) throw new Error("cloud.envFile must be a canonical repository-relative path");
|
|
299
|
-
const seen = /* @__PURE__ */ new Set();
|
|
300
314
|
return {
|
|
301
|
-
services
|
|
302
|
-
if (!cloudService(service)) throw new Error("cloud.services contains an unsupported service");
|
|
303
|
-
if (seen.has(service)) throw new Error(`cloud.services contains duplicate service: ${service}`);
|
|
304
|
-
seen.add(service);
|
|
305
|
-
return service;
|
|
306
|
-
}),
|
|
315
|
+
services,
|
|
307
316
|
install: cloudInstallCommand,
|
|
308
317
|
envFile
|
|
309
318
|
};
|
|
@@ -515,8 +524,7 @@ NODE_DIR="/opt/stll-cloud/node/$NODE_VERSION"
|
|
|
515
524
|
BUN_DIR="/opt/stll-cloud/bun/$BUN_VERSION"
|
|
516
525
|
export PATH="$NODE_DIR/bin:$BUN_DIR:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
517
526
|
BUN_CACHE="/var/cache/stll-cloud/bun/$OUTPUT_UID"
|
|
518
|
-
|
|
519
|
-
ENV_MARKER='@ENV_MARKER@'
|
|
527
|
+
@ENV_ASSIGNMENTS@
|
|
520
528
|
[[ -f "$REPO_ROOT/.node-version" && ! -L "$REPO_ROOT/.node-version" ]] || fail 'A regular root .node-version is required'
|
|
521
529
|
[[ "$(awk 'NF { if (++count > 1 || NF != 1) exit 1; value=$1 } END { if (count != 1) exit 1; print value }' "$REPO_ROOT/.node-version")" == "$NODE_VERSION" ]] || fail 'Node selector changed; regenerate cloud-setup.sh'
|
|
522
530
|
cd -- "$REPO_ROOT"
|
|
@@ -585,6 +593,21 @@ dependency_install() {
|
|
|
585
593
|
@SERVICE_INSTALL@
|
|
586
594
|
@SERVICE_START@
|
|
587
595
|
|
|
596
|
+
@ENV_FUNCTIONS@
|
|
597
|
+
|
|
598
|
+
if [[ "$MODE" == install ]]; then
|
|
599
|
+
install_runtimes
|
|
600
|
+
install_services
|
|
601
|
+
dependency_install
|
|
602
|
+
exit 0
|
|
603
|
+
fi
|
|
604
|
+
runtime_ready
|
|
605
|
+
@ENV_VALIDATE@
|
|
606
|
+
prepare_state
|
|
607
|
+
start_services
|
|
608
|
+
@ENV_WRITE@
|
|
609
|
+
`;
|
|
610
|
+
const environmentFunctions = String.raw`
|
|
588
611
|
validate_environment() {
|
|
589
612
|
local part target="$REPO_ROOT" env_path="$REPO_ROOT/$ENV_FILE" first_line temporary
|
|
590
613
|
while IFS= read -r part; do
|
|
@@ -611,20 +634,6 @@ write_environment() {
|
|
|
611
634
|
chown "$OUTPUT_UID:$OUTPUT_GID" "$temporary"
|
|
612
635
|
mv -f -- "$temporary" "$env_path"
|
|
613
636
|
}
|
|
614
|
-
|
|
615
|
-
if [[ "$MODE" == install ]]; then
|
|
616
|
-
install_runtimes
|
|
617
|
-
install_services
|
|
618
|
-
dependency_install
|
|
619
|
-
exit 0
|
|
620
|
-
fi
|
|
621
|
-
runtime_ready
|
|
622
|
-
DATABASE_URL=''
|
|
623
|
-
REDIS_URL=''
|
|
624
|
-
validate_environment
|
|
625
|
-
prepare_state
|
|
626
|
-
start_services
|
|
627
|
-
write_environment
|
|
628
637
|
`;
|
|
629
638
|
const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
630
639
|
const generateCloudSetup = ({ policy, cloud, nodeVersion }) => {
|
|
@@ -636,8 +645,10 @@ const generateCloudSetup = ({ policy, cloud, nodeVersion }) => {
|
|
|
636
645
|
const replacements = /* @__PURE__ */ new Map([
|
|
637
646
|
["@BUN@", policy.bun],
|
|
638
647
|
["@NODE@", exactNode],
|
|
639
|
-
["@
|
|
640
|
-
["@
|
|
648
|
+
["@ENV_ASSIGNMENTS@", selected.envFile === void 0 ? "" : `ENV_FILE=${shellQuote(selected.envFile)}\nENV_MARKER=${shellQuote(cloudEnvironmentMarker)}`],
|
|
649
|
+
["@ENV_FUNCTIONS@", selected.envFile === void 0 ? "" : environmentFunctions],
|
|
650
|
+
["@ENV_VALIDATE@", selected.envFile === void 0 ? "" : "DATABASE_URL=''\nREDIS_URL=''\nvalidate_environment"],
|
|
651
|
+
["@ENV_WRITE@", selected.envFile === void 0 ? "" : "write_environment"],
|
|
641
652
|
["@SERVICE_INSTALL@", renderCloudServiceInstall({
|
|
642
653
|
services,
|
|
643
654
|
postgres: policy.postgres,
|
|
@@ -2198,31 +2209,71 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2198
2209
|
});
|
|
2199
2210
|
});
|
|
2200
2211
|
const checkRuntimeImage = ({ image, line, label }) => {
|
|
2201
|
-
|
|
2212
|
+
const unknownRuntime = () => {
|
|
2202
2213
|
add({
|
|
2203
2214
|
rule: "runtime-docker",
|
|
2204
2215
|
line,
|
|
2205
2216
|
message: `cannot determine ${label} runtime; use a static image or a reasoned runtime-docker opt-out`
|
|
2206
2217
|
});
|
|
2218
|
+
};
|
|
2219
|
+
if (typeof image !== "string") {
|
|
2220
|
+
unknownRuntime();
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
const canonical = canonicalDockerRuntime(image);
|
|
2224
|
+
const structuralImage = canonical.replace(/\$\{[^}]*\}|\$[A-Za-z_][A-Za-z_\d]*/g, (variable) => "x".repeat(variable.length));
|
|
2225
|
+
const digestSeparator = structuralImage.indexOf("@");
|
|
2226
|
+
const reference = digestSeparator < 0 ? canonical : canonical.slice(0, digestSeparator);
|
|
2227
|
+
const structuralReference = digestSeparator < 0 ? structuralImage : structuralImage.slice(0, digestSeparator);
|
|
2228
|
+
const managedFamily = /^(node|python|oven\/bun)(?=:|$)/i.exec(structuralReference)?.[1];
|
|
2229
|
+
const tagSeparator = managedFamily === void 0 ? structuralReference.lastIndexOf(":") : structuralReference.indexOf(":", managedFamily.length);
|
|
2230
|
+
const hasTag = managedFamily === void 0 ? tagSeparator > structuralReference.lastIndexOf("/") : tagSeparator !== -1;
|
|
2231
|
+
const imageRepository = hasTag ? reference.slice(0, tagSeparator) : reference;
|
|
2232
|
+
if (imageRepository.length === 0 || imageRepository.includes("$")) {
|
|
2233
|
+
unknownRuntime();
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
if (!/^(node|python|oven\/bun)$/i.test(imageRepository)) return;
|
|
2237
|
+
const tool = imageRepository.toLowerCase();
|
|
2238
|
+
if (image.includes("$")) {
|
|
2239
|
+
unknownRuntime();
|
|
2207
2240
|
return;
|
|
2208
2241
|
}
|
|
2209
|
-
const
|
|
2210
|
-
if (
|
|
2211
|
-
const tool = runtime[1]?.toLowerCase();
|
|
2212
|
-
if (image.includes("@")) {
|
|
2242
|
+
const digest = digestSeparator < 0 ? void 0 : canonical.slice(digestSeparator + 1);
|
|
2243
|
+
if (digest !== void 0 && !/^sha256:[a-fA-F\d]{64}$/.test(digest)) {
|
|
2213
2244
|
add({
|
|
2214
2245
|
rule: tool === "oven/bun" ? "bun-pins" : "runtime-docker",
|
|
2215
2246
|
line,
|
|
2216
|
-
message: `${label} ${tool} digest
|
|
2247
|
+
message: `${label} ${tool} digest must be sha256 followed by 64 hexadecimal characters`
|
|
2217
2248
|
});
|
|
2218
2249
|
return;
|
|
2219
2250
|
}
|
|
2220
|
-
const tag =
|
|
2251
|
+
const tag = hasTag ? reference.slice(tagSeparator + 1) : void 0;
|
|
2221
2252
|
const variantSuffix = tool === "oven/bun" ? /-(?:alpine|slim|debian)$/ : /-(?:alpine(?:\d+(?:\.\d+)*)?|(?:bookworm|bullseye|trixie|buster)(?:-slim)?|slim)$/;
|
|
2222
2253
|
const version = tag?.replace(variantSuffix, "");
|
|
2223
2254
|
let expected = policy.bun;
|
|
2224
2255
|
if (tool === "node") expected = policy.node;
|
|
2225
2256
|
else if (tool === "python") expected = policy.python;
|
|
2257
|
+
if (digest !== void 0) {
|
|
2258
|
+
if (version === void 0 || !/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/.test(version)) {
|
|
2259
|
+
add({
|
|
2260
|
+
rule: tool === "oven/bun" ? "bun-pins" : "runtime-docker",
|
|
2261
|
+
line,
|
|
2262
|
+
message: `${label} ${tool} digest requires an exact stable patch tag matching ${expected}`
|
|
2263
|
+
});
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
if (tool === "node" && trackedFiles.has(".node-version")) {
|
|
2267
|
+
if (readFile(".node-version")?.trim() !== version) {
|
|
2268
|
+
add({
|
|
2269
|
+
rule: "runtime-docker",
|
|
2270
|
+
line,
|
|
2271
|
+
message: `${label} node digest tag must match tracked .node-version, found ${version}`
|
|
2272
|
+
});
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2226
2277
|
pin({
|
|
2227
2278
|
rule: tool === "oven/bun" ? "bun-pins" : "runtime-docker",
|
|
2228
2279
|
value: version,
|
|
@@ -2335,14 +2386,6 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2335
2386
|
if (from === null || from[1] === void 0) return;
|
|
2336
2387
|
argumentScope = "stage";
|
|
2337
2388
|
const image = expand(from[1]);
|
|
2338
|
-
if (image.includes("$")) {
|
|
2339
|
-
add({
|
|
2340
|
-
rule: "runtime-docker",
|
|
2341
|
-
line,
|
|
2342
|
-
message: "cannot determine FROM runtime; declare an image ARG default or a reasoned runtime-docker opt-out"
|
|
2343
|
-
});
|
|
2344
|
-
return;
|
|
2345
|
-
}
|
|
2346
2389
|
const stage = from[2]?.toLowerCase();
|
|
2347
2390
|
const previousStage = stages.has(image.toLowerCase());
|
|
2348
2391
|
if (stage !== void 0) stages.add(stage);
|