@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 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 within the declared list: use `[]` for runtimes and
202
- dependencies only. No `cloud` declaration means no cloud setup script.
203
- `envFile` must be a canonical repository-relative path; its existing parent
204
- must be safe at runtime. Track an exact stable Node patch in root
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
@@ -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
- if (!("services" in input) || !Array.isArray(input.services)) throw new Error("cloud.services must be an explicit array");
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: Array.from(input.services, (service) => {
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
- ENV_FILE=@ENV_FILE@
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
- ["@ENV_FILE@", shellQuote(selected.envFile)],
640
- ["@ENV_MARKER@", cloudEnvironmentMarker],
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
- if (typeof image !== "string" || image.includes("$")) {
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 runtime = /^(node|python|oven\/bun)(?::([^@]+))?(?:@.*)?$/i.exec(canonicalDockerRuntime(image));
2210
- if (runtime === null) return;
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 requires an approved digest policy; use a tag-only runtime image`
2247
+ message: `${label} ${tool} digest must be sha256 followed by 64 hexadecimal characters`
2217
2248
  });
2218
2249
  return;
2219
2250
  }
2220
- const tag = runtime[2];
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);