@stll/oxlint-config 0.12.0 → 0.12.2

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.
@@ -267,19 +267,28 @@ const parseCloudSetup = (input) => {
267
267
  if (input === void 0) return void 0;
268
268
  if (typeof input !== "object" || input === null || Array.isArray(input)) throw new Error("cloud must be an object");
269
269
  for (const key of Object.keys(input)) if (key !== "services" && key !== "install" && key !== "envFile") throw new Error(`unsupported cloud key: ${key}`);
270
- if (!("services" in input) || !Array.isArray(input.services)) throw new Error("cloud.services must be an explicit array");
270
+ const selected = "services" in input ? input.services : [];
271
+ if (!Array.isArray(selected)) throw new Error("cloud.services must be an array when provided");
271
272
  if (!("install" in input) || input.install !== "bun install --frozen-lockfile") throw new Error(`cloud.install must be exactly ${cloudInstallCommand}`);
273
+ const seen = /* @__PURE__ */ new Set();
274
+ const services = Array.from(selected, (service) => {
275
+ if (!cloudService(service)) throw new Error("cloud.services contains an unsupported service");
276
+ if (seen.has(service)) throw new Error(`cloud.services contains duplicate service: ${service}`);
277
+ seen.add(service);
278
+ return service;
279
+ });
280
+ if (services.length === 0) {
281
+ if ("envFile" in input) throw new Error("cloud.envFile is not allowed without services");
282
+ return {
283
+ services,
284
+ install: cloudInstallCommand
285
+ };
286
+ }
272
287
  if (!("envFile" in input) || typeof input.envFile !== "string") throw new Error("cloud.envFile must be a canonical repository-relative path");
273
288
  const envFile = input.envFile;
274
289
  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");
275
- const seen = /* @__PURE__ */ new Set();
276
290
  return {
277
- services: Array.from(input.services, (service) => {
278
- if (!cloudService(service)) throw new Error("cloud.services contains an unsupported service");
279
- if (seen.has(service)) throw new Error(`cloud.services contains duplicate service: ${service}`);
280
- seen.add(service);
281
- return service;
282
- }),
291
+ services,
283
292
  install: cloudInstallCommand,
284
293
  envFile
285
294
  };
@@ -491,8 +500,7 @@ NODE_DIR="/opt/stll-cloud/node/$NODE_VERSION"
491
500
  BUN_DIR="/opt/stll-cloud/bun/$BUN_VERSION"
492
501
  export PATH="$NODE_DIR/bin:$BUN_DIR:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
493
502
  BUN_CACHE="/var/cache/stll-cloud/bun/$OUTPUT_UID"
494
- ENV_FILE=@ENV_FILE@
495
- ENV_MARKER='@ENV_MARKER@'
503
+ @ENV_ASSIGNMENTS@
496
504
  [[ -f "$REPO_ROOT/.node-version" && ! -L "$REPO_ROOT/.node-version" ]] || fail 'A regular root .node-version is required'
497
505
  [[ "$(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'
498
506
  cd -- "$REPO_ROOT"
@@ -561,6 +569,21 @@ dependency_install() {
561
569
  @SERVICE_INSTALL@
562
570
  @SERVICE_START@
563
571
 
572
+ @ENV_FUNCTIONS@
573
+
574
+ if [[ "$MODE" == install ]]; then
575
+ install_runtimes
576
+ install_services
577
+ dependency_install
578
+ exit 0
579
+ fi
580
+ runtime_ready
581
+ @ENV_VALIDATE@
582
+ prepare_state
583
+ start_services
584
+ @ENV_WRITE@
585
+ `;
586
+ const environmentFunctions = String.raw`
564
587
  validate_environment() {
565
588
  local part target="$REPO_ROOT" env_path="$REPO_ROOT/$ENV_FILE" first_line temporary
566
589
  while IFS= read -r part; do
@@ -587,20 +610,6 @@ write_environment() {
587
610
  chown "$OUTPUT_UID:$OUTPUT_GID" "$temporary"
588
611
  mv -f -- "$temporary" "$env_path"
589
612
  }
590
-
591
- if [[ "$MODE" == install ]]; then
592
- install_runtimes
593
- install_services
594
- dependency_install
595
- exit 0
596
- fi
597
- runtime_ready
598
- DATABASE_URL=''
599
- REDIS_URL=''
600
- validate_environment
601
- prepare_state
602
- start_services
603
- write_environment
604
613
  `;
605
614
  const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
606
615
  const generateCloudSetup = ({ policy, cloud, nodeVersion }) => {
@@ -612,8 +621,10 @@ const generateCloudSetup = ({ policy, cloud, nodeVersion }) => {
612
621
  const replacements = /* @__PURE__ */ new Map([
613
622
  ["@BUN@", policy.bun],
614
623
  ["@NODE@", exactNode],
615
- ["@ENV_FILE@", shellQuote(selected.envFile)],
616
- ["@ENV_MARKER@", cloudEnvironmentMarker],
624
+ ["@ENV_ASSIGNMENTS@", selected.envFile === void 0 ? "" : `ENV_FILE=${shellQuote(selected.envFile)}\nENV_MARKER=${shellQuote(cloudEnvironmentMarker)}`],
625
+ ["@ENV_FUNCTIONS@", selected.envFile === void 0 ? "" : environmentFunctions],
626
+ ["@ENV_VALIDATE@", selected.envFile === void 0 ? "" : "DATABASE_URL=''\nREDIS_URL=''\nvalidate_environment"],
627
+ ["@ENV_WRITE@", selected.envFile === void 0 ? "" : "write_environment"],
617
628
  ["@SERVICE_INSTALL@", renderCloudServiceInstall({
618
629
  services,
619
630
  postgres: policy.postgres,
@@ -634,7 +645,7 @@ const generateCloudSetup = ({ policy, cloud, nodeVersion }) => {
634
645
  };
635
646
  //#endregion
636
647
  //#region src/toolchain-container-inputs.ts
637
- const record$6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
648
+ const record$7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
638
649
  /** Filename class recognized by Dependabot's Docker Compose fetcher. */
639
650
  const isComposeDefinitionPath = (file) => /^\.?(?:docker-)?compose(?:-[\w]+)?(?:\.[\w-]+)?\.ya?ml$/i.test(file.split("/").at(-1) ?? "");
640
651
  /** Candidate YAML files are classified by their document shape after reading. */
@@ -681,11 +692,11 @@ const kubernetesPodSpecPaths = {
681
692
  };
682
693
  /** Shared semantic image enumeration for runtime checks and update-root generation. */
683
694
  const containerDocumentImages = (document, file) => {
684
- if (!record$6(document)) return void 0;
695
+ if (!record$7(document)) return void 0;
685
696
  const services = document["services"];
686
- if (isComposeDefinitionPath(file) && record$6(services)) {
697
+ if (isComposeDefinitionPath(file) && record$7(services)) {
687
698
  const images = [];
688
- for (const [name, service] of Object.entries(services)) if (record$6(service) && service["image"] !== void 0) images.push({
699
+ for (const [name, service] of Object.entries(services)) if (record$7(service) && service["image"] !== void 0) images.push({
689
700
  image: service["image"],
690
701
  path: [
691
702
  "services",
@@ -706,7 +717,7 @@ const containerDocumentImages = (document, file) => {
706
717
  visited.add(resource);
707
718
  if (typeof resource["apiVersion"] !== "string" || typeof resource["kind"] !== "string") return;
708
719
  if (resource["kind"] === "List" && Array.isArray(resource["items"])) {
709
- for (const [index, item] of resource["items"].entries()) if (record$6(item)) walkResource(item, [
720
+ for (const [index, item] of resource["items"].entries()) if (record$7(item)) walkResource(item, [
710
721
  ...prefix,
711
722
  "items",
712
723
  index
@@ -716,8 +727,8 @@ const containerDocumentImages = (document, file) => {
716
727
  const specPath = Object.entries(kubernetesPodSpecPaths).find(([kind]) => kind === resource["kind"])?.[1];
717
728
  if (specPath === void 0) return;
718
729
  let spec = resource;
719
- for (const key of specPath) spec = record$6(spec) ? spec[key] : void 0;
720
- if (!record$6(spec)) return;
730
+ for (const key of specPath) spec = record$7(spec) ? spec[key] : void 0;
731
+ if (!record$7(spec)) return;
721
732
  for (const key of [
722
733
  "containers",
723
734
  "initContainers",
@@ -725,7 +736,7 @@ const containerDocumentImages = (document, file) => {
725
736
  ]) {
726
737
  const containers = spec[key];
727
738
  if (!Array.isArray(containers)) continue;
728
- for (const [index, container] of containers.entries()) if (record$6(container) && container["image"] !== void 0) images.push({
739
+ for (const [index, container] of containers.entries()) if (record$7(container) && container["image"] !== void 0) images.push({
729
740
  image: container["image"],
730
741
  path: [
731
742
  ...prefix,
@@ -828,7 +839,7 @@ const workspaceContains = ({ directory, file, patterns, rootMembership }) => {
828
839
  //#endregion
829
840
  //#region src/toolchain-dependabot.ts
830
841
  const dependabotRules = ["dependabot-policy"];
831
- const record$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
842
+ const record$6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
832
843
  const strings = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
833
844
  const sameStrings = (value, expected) => {
834
845
  if (!strings(value) || value.length !== expected.length) return false;
@@ -850,8 +861,8 @@ const workspaceTable = (parsed, ecosystem) => {
850
861
  case "cargo": return parsed["workspace"];
851
862
  case "uv": {
852
863
  const tool = parsed["tool"];
853
- const uv = record$5(tool) ? tool["uv"] : void 0;
854
- return record$5(uv) ? uv["workspace"] : void 0;
864
+ const uv = record$6(tool) ? tool["uv"] : void 0;
865
+ return record$6(uv) ? uv["workspace"] : void 0;
855
866
  }
856
867
  default: throw new Error("unknown workspace ecosystem", { cause: ecosystem });
857
868
  }
@@ -871,7 +882,7 @@ const workspaceRoots = (files, ecosystem) => {
871
882
  const document = parseDocument(text);
872
883
  if (document.errors.length > 0) throw new Error(`${workspaceFile}:1: invalid pnpm workspace YAML`);
873
884
  const parsed = document.toJS({ maxAliasCount: 100 });
874
- if (!record$5(parsed)) throw new Error(`${workspaceFile}:1: pnpm workspace must contain an object`);
885
+ if (!record$6(parsed)) throw new Error(`${workspaceFile}:1: pnpm workspace must contain an object`);
875
886
  const patterns = parsed["packages"] ?? [];
876
887
  if (!strings(patterns)) throw new Error(`${workspaceFile}:1: pnpm packages must be a string array`);
877
888
  workspaces.set(directory, patterns.map((pattern) => pattern.startsWith("!") ? `!${path.posix.normalize(pattern.slice(1))}` : path.posix.normalize(pattern)));
@@ -883,13 +894,13 @@ const workspaceRoots = (files, ecosystem) => {
883
894
  }
884
895
  try {
885
896
  const parsed = ecosystem === "npm" ? JSON.parse(files[file] ?? "") : parse(files[file] ?? "");
886
- if (!record$5(parsed)) continue;
897
+ if (!record$6(parsed)) continue;
887
898
  if (ecosystem === "npm" && typeof parsed["packageManager"] === "string" && parsed["packageManager"].startsWith("pnpm@")) continue;
888
899
  const value = workspaceTable(parsed, ecosystem);
889
- const patterns = record$5(value) ? value[ecosystem === "npm" ? "packages" : "members"] : value;
890
- const members = ecosystem === "cargo" && record$5(value) && patterns === void 0 ? [] : patterns;
900
+ const patterns = record$6(value) ? value[ecosystem === "npm" ? "packages" : "members"] : value;
901
+ const members = ecosystem === "cargo" && record$6(value) && patterns === void 0 ? [] : patterns;
891
902
  if (strings(members)) {
892
- const excluded = ecosystem !== "npm" && record$5(value) && strings(value["exclude"]) ? value["exclude"].map((pattern) => `!${pattern}`) : [];
903
+ const excluded = ecosystem !== "npm" && record$6(value) && strings(value["exclude"]) ? value["exclude"].map((pattern) => `!${pattern}`) : [];
893
904
  workspaces.set(path.posix.dirname(file), [...members, ...excluded]);
894
905
  }
895
906
  } catch {}
@@ -940,11 +951,11 @@ const workspaceRoots = (files, ecosystem) => {
940
951
  for (const directory of memberDirectories) try {
941
952
  const parsed = parse(files[path.posix.join(directory, "Cargo.toml")] ?? "");
942
953
  const workspace = parse(files[path.posix.join(root, "Cargo.toml")] ?? "")["workspace"];
943
- const inherited = record$5(workspace) ? workspace["dependencies"] : void 0;
954
+ const inherited = record$6(workspace) ? workspace["dependencies"] : void 0;
944
955
  const dependencyOwners = [parsed];
945
956
  const targets = parsed["target"];
946
- if (record$5(targets)) {
947
- for (const target of Object.values(targets)) if (record$5(target)) dependencyOwners.push(target);
957
+ if (record$6(targets)) {
958
+ for (const target of Object.values(targets)) if (record$6(target)) dependencyOwners.push(target);
948
959
  }
949
960
  for (const owner of dependencyOwners) for (const key of [
950
961
  "dependencies",
@@ -952,12 +963,12 @@ const workspaceRoots = (files, ecosystem) => {
952
963
  "build-dependencies"
953
964
  ]) {
954
965
  const dependencies = owner[key];
955
- if (!record$5(dependencies)) continue;
966
+ if (!record$6(dependencies)) continue;
956
967
  for (const [name, dependency] of Object.entries(dependencies)) {
957
- if (!record$5(dependency)) continue;
968
+ if (!record$6(dependency)) continue;
958
969
  const isInherited = dependency["workspace"] === true;
959
- const resolved = isInherited && record$5(inherited) ? inherited[name] : dependency;
960
- if (!record$5(resolved) || typeof resolved["path"] !== "string") continue;
970
+ const resolved = isInherited && record$6(inherited) ? inherited[name] : dependency;
971
+ if (!record$6(resolved) || typeof resolved["path"] !== "string") continue;
961
972
  if (path.posix.isAbsolute(resolved["path"]) || /\\|\$|^[A-Za-z]:/.test(resolved["path"])) continue;
962
973
  const target = path.posix.normalize(path.posix.join(isInherited ? root : directory, resolved["path"]));
963
974
  const relative = path.posix.relative(root, target);
@@ -985,7 +996,7 @@ const javascriptEcosystem = (directory, files) => {
985
996
  if (files[path.posix.join(root, "bun.lock")] !== void 0) return "bun";
986
997
  try {
987
998
  const manifest = JSON.parse(files[path.posix.join(root, "package.json")] ?? "{}");
988
- if (record$5(manifest) && typeof manifest["packageManager"] === "string" && manifest["packageManager"].startsWith("bun@")) return "bun";
999
+ if (record$6(manifest) && typeof manifest["packageManager"] === "string" && manifest["packageManager"].startsWith("bun@")) return "bun";
989
1000
  } catch {}
990
1001
  return "npm";
991
1002
  };
@@ -997,7 +1008,7 @@ const pythonEcosystem = (file, files) => {
997
1008
  const pyproject = files[path.posix.join(root, "pyproject.toml")];
998
1009
  if (pyproject !== void 0) try {
999
1010
  const tool = parse(pyproject)["tool"];
1000
- if (record$5(tool) && record$5(tool["uv"])) return "uv";
1011
+ if (record$6(tool) && record$6(tool["uv"])) return "uv";
1001
1012
  } catch {}
1002
1013
  return "pip";
1003
1014
  };
@@ -1068,7 +1079,7 @@ const checkDependabot = ({ files, policy }) => {
1068
1079
  add("cannot resolve Dependabot YAML aliases");
1069
1080
  return diagnostics;
1070
1081
  }
1071
- if (!record$5(parsed) || parsed["version"] !== 2 || !Array.isArray(parsed["updates"])) {
1082
+ if (!record$6(parsed) || parsed["version"] !== 2 || !Array.isArray(parsed["updates"])) {
1072
1083
  add("Dependabot must declare version: 2 and an updates array");
1073
1084
  return diagnostics;
1074
1085
  }
@@ -1081,7 +1092,7 @@ const checkDependabot = ({ files, policy }) => {
1081
1092
  });
1082
1093
  if (legacyDocument.errors.length === 0) legacy = legacyDocument.toJS({ maxAliasCount: 100 });
1083
1094
  } catch {}
1084
- const legacyUpdates = record$5(legacy) && Array.isArray(legacy["updates"]) ? legacy["updates"] : [];
1095
+ const legacyUpdates = record$6(legacy) && Array.isArray(legacy["updates"]) ? legacy["updates"] : [];
1085
1096
  const rootClaims = /* @__PURE__ */ new Map();
1086
1097
  const directoryClaims = /* @__PURE__ */ new Map();
1087
1098
  for (const [index, update] of parsed["updates"].entries()) {
@@ -1095,7 +1106,7 @@ const checkDependabot = ({ files, policy }) => {
1095
1106
  return offset === void 0 ? 1 : lineCounter.linePos(offset).line;
1096
1107
  };
1097
1108
  const report = (key, message) => add(message, lineOf(key));
1098
- if (!record$5(update) || typeof update["package-ecosystem"] !== "string") {
1109
+ if (!record$6(update) || typeof update["package-ecosystem"] !== "string") {
1099
1110
  report("package-ecosystem", "each Dependabot update must declare a package ecosystem");
1100
1111
  continue;
1101
1112
  }
@@ -1129,26 +1140,75 @@ const checkDependabot = ({ files, policy }) => {
1129
1140
  covered.set(ecosystem, matches);
1130
1141
  const schedule = update["schedule"];
1131
1142
  const legacyUpdate = legacyUpdates.at(index);
1132
- const legacySchedule = record$5(legacyUpdate) ? legacyUpdate["schedule"] : void 0;
1133
- if (record$5(schedule) && schedule["time"] === policy.schedule.time && (!record$5(legacySchedule) || legacySchedule["time"] !== policy.schedule.time)) report("schedule", `${ecosystem}: quote schedule time to preserve its string value in YAML 1.1`);
1134
- if (!record$5(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`);
1143
+ const legacySchedule = record$6(legacyUpdate) ? legacyUpdate["schedule"] : void 0;
1144
+ if (record$6(schedule) && schedule["time"] === policy.schedule.time && (!record$6(legacySchedule) || legacySchedule["time"] !== policy.schedule.time)) report("schedule", `${ecosystem}: quote schedule time to preserve its string value in YAML 1.1`);
1145
+ if (!record$6(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`);
1135
1146
  const cooldown = update["cooldown"];
1136
- if (!record$5(cooldown) || Object.keys(cooldown).length !== 1 || cooldown["default-days"] !== policy.cooldown.defaultDays) report("cooldown", `${ecosystem}: cooldown must contain only default-days: ${policy.cooldown.defaultDays}`);
1147
+ if (!record$6(cooldown) || Object.keys(cooldown).length !== 1 || cooldown["default-days"] !== policy.cooldown.defaultDays) report("cooldown", `${ecosystem}: cooldown must contain only default-days: ${policy.cooldown.defaultDays}`);
1137
1148
  const groups = update["groups"];
1138
- if (!record$5(groups) || Object.keys(groups).length !== Object.keys(policy.groups).length || Object.entries(policy.groups).some(([name, expected]) => {
1149
+ if (!record$6(groups) || Object.keys(groups).length !== Object.keys(policy.groups).length || Object.entries(policy.groups).some(([name, expected]) => {
1139
1150
  const group = groups[name];
1140
- return !record$5(group) || Object.keys(group).length !== 2 || !sameStrings(group["patterns"], expected.patterns) || !sameStrings(group["update-types"], expected.updateTypes);
1151
+ return !record$6(group) || Object.keys(group).length !== 2 || !sameStrings(group["patterns"], expected.patterns) || !sameStrings(group["update-types"], expected.updateTypes);
1141
1152
  })) report("groups", `${ecosystem}: groups must match the shared policy`);
1142
1153
  const ignored = ignoredFor(ecosystem, policy);
1143
1154
  const ignore = update["ignore"];
1144
1155
  if (ignore !== void 0 && (!Array.isArray(ignore) || ignore.length === 0)) report("ignore", `${ecosystem}: omit ignore or provide a nonempty array`);
1145
- if (Array.isArray(ignore) && ignore.some((entry) => !record$5(entry) || typeof entry["dependency-name"] !== "string" || !ignored.includes(entry["dependency-name"]))) report("ignore", `${ecosystem}: ignore only packages owned by the shared policy`);
1146
- for (const name of ignored) if (!Array.isArray(ignore) || !ignore.some((entry) => record$5(entry) && entry["dependency-name"] === name && Object.keys(entry).length === 1)) report("ignore", `${ecosystem}: ignore all updates for shared pin ${name}`);
1156
+ if (Array.isArray(ignore) && ignore.some((entry) => !record$6(entry) || typeof entry["dependency-name"] !== "string" || !ignored.includes(entry["dependency-name"]))) report("ignore", `${ecosystem}: ignore only packages owned by the shared policy`);
1157
+ for (const name of ignored) if (!Array.isArray(ignore) || !ignore.some((entry) => record$6(entry) && entry["dependency-name"] === name && Object.keys(entry).length === 1)) report("ignore", `${ecosystem}: ignore all updates for shared pin ${name}`);
1147
1158
  }
1148
1159
  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}`);
1149
1160
  return diagnostics;
1150
1161
  };
1151
1162
  //#endregion
1163
+ //#region src/toolchain-dynamic-selectors.ts
1164
+ const locatorValidators = {
1165
+ image: /^(?:jobs\.[\w-]+\.(?:container|services\.[\w-]+)|services\.[\w.-]+\.image)$/,
1166
+ "bun-source": /^(?:jobs\.[\w-]+|runs)\.steps\.[\w-]+$/
1167
+ };
1168
+ const knownKind = (value) => typeof value === "string" && Object.hasOwn(locatorValidators, value);
1169
+ const record$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1170
+ /** One reviewed declaration identifies one unresolved selector, never a rule. */
1171
+ const parseDynamicSelectors = (input) => {
1172
+ if (input === void 0) return [];
1173
+ if (!Array.isArray(input)) throw new Error("dynamicSelectors must be an array");
1174
+ const result = [];
1175
+ const keys = /* @__PURE__ */ new Set();
1176
+ for (const entry of input) {
1177
+ if (!record$5(entry) || Object.keys(entry).some((key) => ![
1178
+ "path",
1179
+ "kind",
1180
+ "reason",
1181
+ "at",
1182
+ "line"
1183
+ ].includes(key)) || typeof entry["path"] !== "string" || entry["path"] === "" || /[\\:$*?]|[^\S ]/.test(entry["path"]) || entry["path"].split("/").some((part) => part === "" || part === "." || part === "..") || typeof entry["reason"] !== "string" || entry["reason"].trim() === "") throw new Error("each dynamic selector requires an exact repository path, known kind and nonempty reason");
1184
+ const kind = entry["kind"];
1185
+ if (!knownKind(kind)) throw new Error("unknown dynamic selector kind");
1186
+ let selector;
1187
+ if (Object.hasOwn(entry, "line") && !Object.hasOwn(entry, "at") && typeof entry["line"] === "number" && Number.isSafeInteger(entry["line"]) && entry["line"] > 0) selector = {
1188
+ path: entry["path"],
1189
+ kind,
1190
+ reason: entry["reason"],
1191
+ line: entry["line"]
1192
+ };
1193
+ else if (!Object.hasOwn(entry, "line") && typeof entry["at"] === "string" && locatorValidators[kind].test(entry["at"])) selector = {
1194
+ path: entry["path"],
1195
+ kind,
1196
+ reason: entry["reason"],
1197
+ at: entry["at"]
1198
+ };
1199
+ else throw new Error("dynamic selector requires one positive line or a kind-specific job/service/step ID locator");
1200
+ const key = JSON.stringify([
1201
+ selector.path,
1202
+ selector.kind,
1203
+ selector.at ?? selector.line
1204
+ ]);
1205
+ if (keys.has(key)) throw new Error("dynamic selector locations must be unique");
1206
+ keys.add(key);
1207
+ result.push(selector);
1208
+ }
1209
+ return result;
1210
+ };
1211
+ //#endregion
1152
1212
  //#region src/toolchain-engine-floors.ts
1153
1213
  const record$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1154
1214
  const repositoryPath = (value) => typeof value === "string" && value !== "" && !/^[A-Za-z]:/.test(value) && !/[\\\s]/.test(value) && !Array.from(value).some((character) => character.charCodeAt(0) < 32) && value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
@@ -1960,7 +2020,7 @@ const pythonRequirementMatches = (requirement, version) => {
1960
2020
  });
1961
2021
  };
1962
2022
  /** Check runtime declarations only in the files that declare or configure them. */
1963
- const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, repository, onDelegated, engineFloors = [], onEngineFloor }) => {
2023
+ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, repository, onDelegated, dynamicSelectors = [], onDynamicSelector, engineFloors = [], onEngineFloor }) => {
1964
2024
  const diagnostics = [];
1965
2025
  const name = path.posix.basename(file);
1966
2026
  const lines = text.split(/\r?\n/);
@@ -1972,6 +2032,16 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1972
2032
  message
1973
2033
  });
1974
2034
  };
2035
+ let currentLocation;
2036
+ const dynamic = (value) => typeof value === "string" && value.includes("$");
2037
+ const authorizeDynamic = (kind, line, at = currentLocation) => {
2038
+ const matches = dynamicSelectors.filter((entry) => entry.path === file && entry.kind === kind && (entry.at !== void 0 ? entry.at === at : entry.line === line));
2039
+ if (matches.length !== 1) return false;
2040
+ const entry = matches.at(0);
2041
+ if (entry === void 0) return false;
2042
+ onDynamicSelector?.(entry);
2043
+ return true;
2044
+ };
1975
2045
  const lineOf = (key) => Math.max(1, lines.findIndex((line) => line.includes(key)) + 1);
1976
2046
  const pin = ({ rule, value, expected, label, line = 1, selector }) => {
1977
2047
  let matches = value === expected;
@@ -2173,32 +2243,73 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2173
2243
  line: index + 1
2174
2244
  });
2175
2245
  });
2176
- const checkRuntimeImage = ({ image, line, label }) => {
2177
- if (typeof image !== "string" || image.includes("$")) {
2246
+ const checkRuntimeImage = ({ image, line, label, at }) => {
2247
+ const unknownRuntime = () => {
2248
+ if (dynamic(image) && authorizeDynamic("image", line, at)) return;
2178
2249
  add({
2179
2250
  rule: "runtime-docker",
2180
2251
  line,
2181
- message: `cannot determine ${label} runtime; use a static image or a reasoned runtime-docker opt-out`
2252
+ message: `cannot determine ${label} runtime; use a static image or a scoped dynamicSelectors declaration`
2182
2253
  });
2254
+ };
2255
+ if (typeof image !== "string") {
2256
+ unknownRuntime();
2257
+ return;
2258
+ }
2259
+ const canonical = canonicalDockerRuntime(image);
2260
+ const structuralImage = canonical.replace(/\$\{[^}]*\}|\$[A-Za-z_][A-Za-z_\d]*/g, (variable) => "x".repeat(variable.length));
2261
+ const digestSeparator = structuralImage.indexOf("@");
2262
+ const reference = digestSeparator < 0 ? canonical : canonical.slice(0, digestSeparator);
2263
+ const structuralReference = digestSeparator < 0 ? structuralImage : structuralImage.slice(0, digestSeparator);
2264
+ const managedFamily = /^(node|python|oven\/bun)(?=:|$)/i.exec(structuralReference)?.[1];
2265
+ const tagSeparator = managedFamily === void 0 ? structuralReference.lastIndexOf(":") : structuralReference.indexOf(":", managedFamily.length);
2266
+ const hasTag = managedFamily === void 0 ? tagSeparator > structuralReference.lastIndexOf("/") : tagSeparator !== -1;
2267
+ const imageRepository = hasTag ? reference.slice(0, tagSeparator) : reference;
2268
+ if (imageRepository.length === 0 || imageRepository.includes("$")) {
2269
+ unknownRuntime();
2270
+ return;
2271
+ }
2272
+ if (!/^(node|python|oven\/bun)$/i.test(imageRepository)) return;
2273
+ const tool = imageRepository.toLowerCase();
2274
+ if (image.includes("$")) {
2275
+ unknownRuntime();
2183
2276
  return;
2184
2277
  }
2185
- const runtime = /^(node|python|oven\/bun)(?::([^@]+))?(?:@.*)?$/i.exec(canonicalDockerRuntime(image));
2186
- if (runtime === null) return;
2187
- const tool = runtime[1]?.toLowerCase();
2188
- if (image.includes("@")) {
2278
+ const digest = digestSeparator < 0 ? void 0 : canonical.slice(digestSeparator + 1);
2279
+ if (digest !== void 0 && !/^sha256:[a-fA-F\d]{64}$/.test(digest)) {
2189
2280
  add({
2190
2281
  rule: tool === "oven/bun" ? "bun-pins" : "runtime-docker",
2191
2282
  line,
2192
- message: `${label} ${tool} digest requires an approved digest policy; use a tag-only runtime image`
2283
+ message: `${label} ${tool} digest must be sha256 followed by 64 hexadecimal characters`
2193
2284
  });
2194
2285
  return;
2195
2286
  }
2196
- const tag = runtime[2];
2287
+ const tag = hasTag ? reference.slice(tagSeparator + 1) : void 0;
2197
2288
  const variantSuffix = tool === "oven/bun" ? /-(?:alpine|slim|debian)$/ : /-(?:alpine(?:\d+(?:\.\d+)*)?|(?:bookworm|bullseye|trixie|buster)(?:-slim)?|slim)$/;
2198
2289
  const version = tag?.replace(variantSuffix, "");
2199
2290
  let expected = policy.bun;
2200
2291
  if (tool === "node") expected = policy.node;
2201
2292
  else if (tool === "python") expected = policy.python;
2293
+ if (digest !== void 0) {
2294
+ if (version === void 0 || !/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/.test(version)) {
2295
+ add({
2296
+ rule: tool === "oven/bun" ? "bun-pins" : "runtime-docker",
2297
+ line,
2298
+ message: `${label} ${tool} digest requires an exact stable patch tag matching ${expected}`
2299
+ });
2300
+ return;
2301
+ }
2302
+ if (tool === "node" && trackedFiles.has(".node-version")) {
2303
+ if (readFile(".node-version")?.trim() !== version) {
2304
+ add({
2305
+ rule: "runtime-docker",
2306
+ line,
2307
+ message: `${label} node digest tag must match tracked .node-version, found ${version}`
2308
+ });
2309
+ return;
2310
+ }
2311
+ }
2312
+ }
2202
2313
  pin({
2203
2314
  rule: tool === "oven/bun" ? "bun-pins" : "runtime-docker",
2204
2315
  value: version,
@@ -2311,14 +2422,6 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2311
2422
  if (from === null || from[1] === void 0) return;
2312
2423
  argumentScope = "stage";
2313
2424
  const image = expand(from[1]);
2314
- if (image.includes("$")) {
2315
- add({
2316
- rule: "runtime-docker",
2317
- line,
2318
- message: "cannot determine FROM runtime; declare an image ARG default or a reasoned runtime-docker opt-out"
2319
- });
2320
- return;
2321
- }
2322
2425
  const stage = from[2]?.toLowerCase();
2323
2426
  const previousStage = stages.has(image.toLowerCase());
2324
2427
  if (stage !== void 0) stages.add(stage);
@@ -2375,6 +2478,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2375
2478
  checkRuntimeImage({
2376
2479
  image: entry.image,
2377
2480
  line,
2481
+ at: entry.path.join("."),
2378
2482
  label: classified?.ecosystem === "docker-compose" ? "Compose image" : "Kubernetes image"
2379
2483
  });
2380
2484
  }
@@ -2409,8 +2513,22 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2409
2513
  let checkoutBindings = [];
2410
2514
  let checkoutDestinations = /* @__PURE__ */ new Map();
2411
2515
  let unknownCheckoutDestination = false;
2516
+ let unreviewableCheckoutSource = false;
2412
2517
  const staticRepositoryPath = (value) => typeof value === "string" && value !== "" && !value.includes("${{") && !value.includes("\\") && !value.includes(":") && !path.posix.isAbsolute(value) && !value.split("/").includes("..");
2413
2518
  const normalizeCheckoutPath = (prefix) => path.posix.normalize(prefix).replace(/\/$/, "") || ".";
2519
+ const checkoutProvidersFor = (selector) => {
2520
+ if (selector === void 0) return checkoutBindings;
2521
+ let deepest;
2522
+ for (const entry of checkoutBindings) {
2523
+ if (entry.path !== "." && selector !== entry.path && !selector.startsWith(`${entry.path}/`)) continue;
2524
+ if (deepest === void 0 || entry.path !== "." && (deepest.path === "." || entry.path.length > deepest.path.length)) deepest = entry;
2525
+ }
2526
+ return checkoutBindings.filter((entry) => entry.path === deepest?.path);
2527
+ };
2528
+ const hasAmbiguousCheckoutWriters = (selector, providers) => {
2529
+ if (providers.length > 0) return providers.some((entry) => (checkoutDestinations.get(entry.path) ?? 0) > 1);
2530
+ return [...checkoutDestinations].some(([prefix, count]) => count > 1 && (selector === void 0 || prefix === "." || selector === prefix || selector.startsWith(`${prefix}/`)));
2531
+ };
2414
2532
  const checkReference = ({ value, tool, line }) => {
2415
2533
  const allowed = {
2416
2534
  node: [".node-version", ".nvmrc"],
@@ -2419,8 +2537,28 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2419
2537
  }[tool];
2420
2538
  const rule = tool === "bun" ? "bun-pins" : "runtime-workflow";
2421
2539
  const selector = staticRepositoryPath(value) ? path.posix.normalize(value) : void 0;
2422
- const binding = selector === void 0 ? void 0 : checkoutBindings.findLast((entry) => entry.path === "." || selector === entry.path || selector.startsWith(`${entry.path}/`));
2540
+ const providers = checkoutProvidersFor(selector);
2541
+ const binding = selector === void 0 ? void 0 : providers.at(-1);
2542
+ const dynamicSource = providers.some((entry) => entry.dynamicSource);
2543
+ const sparseFailure = providers.find((entry) => {
2544
+ if (entry.sparse.mode === "invalid") return true;
2545
+ if (entry.sparse.mode !== "files" && entry.sparse.mode !== "mixed") return false;
2546
+ if (selector === void 0) return true;
2547
+ const selectedFile = entry.path === "." ? selector : path.posix.relative(entry.path, selector);
2548
+ return !entry.sparse.paths.has(selectedFile);
2549
+ });
2550
+ if (sparseFailure !== void 0) {
2551
+ let missingFile = "the selected version file";
2552
+ if (selector !== void 0) missingFile = sparseFailure.path === "." ? selector : path.posix.relative(sparseFailure.path, selector);
2553
+ add({
2554
+ rule,
2555
+ line,
2556
+ message: `checkout sparse-checkout must explicitly list ${missingFile} without invalid literal patterns`
2557
+ });
2558
+ return;
2559
+ }
2423
2560
  if (selector === void 0) {
2561
+ if (tool === "bun" && dynamic(value) && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
2424
2562
  add({
2425
2563
  rule,
2426
2564
  line,
@@ -2428,7 +2566,8 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2428
2566
  });
2429
2567
  return;
2430
2568
  }
2431
- if (unknownCheckoutDestination || [...checkoutDestinations].some(([prefix, count]) => count > 1 && (prefix === "." || selector === prefix || selector.startsWith(`${prefix}/`)))) {
2569
+ if (unknownCheckoutDestination || hasAmbiguousCheckoutWriters(selector, providers)) {
2570
+ if (tool === "bun" && dynamicSource && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
2432
2571
  add({
2433
2572
  rule,
2434
2573
  line,
@@ -2437,6 +2576,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2437
2576
  return;
2438
2577
  }
2439
2578
  if (binding?.source === "untrusted") {
2579
+ if (tool === "bun" && dynamicSource && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
2440
2580
  add({
2441
2581
  rule,
2442
2582
  line: binding.line,
@@ -2453,13 +2593,15 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2453
2593
  });
2454
2594
  return;
2455
2595
  }
2456
- if (binding !== void 0 && (binding.sparse.mode === "invalid" || binding.sparse.mode === "files" && !binding.sparse.paths.has(target))) {
2457
- add({
2458
- rule,
2459
- line,
2460
- message: `checkout sparse-checkout must explicitly list ${target} without dynamic, glob, or negation patterns`
2461
- });
2462
- return;
2596
+ if (binding !== void 0 && (binding.sparse.mode === "dynamic" || binding.sparse.mode === "mixed")) {
2597
+ if (tool !== "bun" || unreviewableCheckoutSource || !authorizeDynamic("bun-source", line)) {
2598
+ add({
2599
+ rule,
2600
+ line,
2601
+ message: `checkout sparse-checkout must explicitly list ${target} without dynamic, glob, or negation patterns`
2602
+ });
2603
+ return;
2604
+ }
2463
2605
  }
2464
2606
  if (binding?.source === "delegated") {
2465
2607
  onDelegated?.({
@@ -2581,10 +2723,10 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2581
2723
  return inputs;
2582
2724
  };
2583
2725
  const getInput = (node, key) => inputsOf(node).get(key.toUpperCase());
2584
- const dynamicRefBody = (value) => {
2585
- if (typeof value !== "string" || !value.startsWith("${{")) return void 0;
2726
+ const expressionAt = (value, start) => {
2727
+ if (!value.startsWith("${{", start)) return void 0;
2586
2728
  let quote;
2587
- for (let index = 3; index < value.length; index++) {
2729
+ for (let index = start + 3; index < value.length; index++) {
2588
2730
  const character = value.at(index);
2589
2731
  if (quote !== void 0) {
2590
2732
  if (character === quote) if (quote === "'" && value.at(index + 1) === "'") index++;
@@ -2594,9 +2736,33 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2594
2736
  }
2595
2737
  if (character === "'" || character === "\"") quote = character;
2596
2738
  else if (value.startsWith("${{", index)) return void 0;
2597
- else if (character === "}" && value.at(index + 1) === "}") return index === value.length - 2 ? value.slice(3, index).trim() : void 0;
2739
+ else if (character === "}" && value.at(index + 1) === "}") return {
2740
+ body: value.slice(start + 3, index).trim(),
2741
+ end: index + 2
2742
+ };
2598
2743
  }
2599
2744
  };
2745
+ const dynamicRefBody = (value) => {
2746
+ if (typeof value !== "string") return void 0;
2747
+ const expression = expressionAt(value, 0);
2748
+ return expression?.end === value.length ? expression.body : void 0;
2749
+ };
2750
+ const sparseEntryIsValid = (entry) => {
2751
+ let literalPath = "";
2752
+ let position = 0;
2753
+ while (position < entry.length) {
2754
+ const start = entry.indexOf("${{", position);
2755
+ const literal = entry.slice(position, start < 0 ? entry.length : start);
2756
+ if (/[!$*?[\]\\:]/.test(literal) || literal.split("/").includes("..")) return false;
2757
+ literalPath += literal;
2758
+ if (start < 0) break;
2759
+ const expression = expressionAt(entry, start);
2760
+ if (expression === void 0 || expression.body === "") return false;
2761
+ literalPath += "expression";
2762
+ position = expression.end;
2763
+ }
2764
+ return staticRepositoryPath(literalPath);
2765
+ };
2600
2766
  const checkoutPrefixOf = (node) => {
2601
2767
  const destination = getInput(node, "path");
2602
2768
  if (destination === void 0) return ".";
@@ -2609,11 +2775,19 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2609
2775
  const value = getInput(node, "sparse-checkout");
2610
2776
  const cone = getInput(node, "sparse-checkout-cone-mode");
2611
2777
  if (!isScalar(value) || typeof value.value !== "string" || cone !== void 0 && (!isScalar(cone) || cone.value !== true && cone.value !== false && cone.value !== "true" && cone.value !== "false")) return { mode: "invalid" };
2612
- const paths = value.value.split(/\r?\n/).map((entry) => entry.trim()).filter((entry) => entry !== "");
2613
- if (paths.length === 0 || paths.some((entry) => !staticRepositoryPath(entry) || entry.startsWith("!") || /[*?[\]]/.test(entry))) return { mode: "invalid" };
2778
+ const paths = value.value.split(/\r?\n/).map((entry) => entry.trim().replace(/^\//, "")).filter((entry) => entry !== "");
2779
+ if (paths.length === 0 || paths.some((entry) => !sparseEntryIsValid(entry))) return { mode: "invalid" };
2780
+ const literalPaths = new Set(paths.filter((entry) => !dynamic(entry)));
2781
+ if (paths.some(dynamic)) {
2782
+ if (literalPaths.size === 0) return { mode: "dynamic" };
2783
+ return {
2784
+ mode: "mixed",
2785
+ paths: literalPaths
2786
+ };
2787
+ }
2614
2788
  return {
2615
2789
  mode: "files",
2616
- paths: new Set(paths)
2790
+ paths: literalPaths
2617
2791
  };
2618
2792
  };
2619
2793
  let currentEngineFloor;
@@ -2643,8 +2817,9 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2643
2817
  }
2644
2818
  if (value.startsWith("./")) {
2645
2819
  const selector = staticRepositoryPath(value) ? path.posix.normalize(value) : void 0;
2646
- const binding = selector === void 0 ? void 0 : checkoutBindings.findLast((entry) => entry.path === "." || selector === entry.path || selector.startsWith(`${entry.path}/`));
2647
- if (selector === void 0 || unknownCheckoutDestination || binding !== void 0 && binding.source !== "tracked" || [...checkoutDestinations].some(([prefix, count]) => count > 1 && (prefix === "." || selector === prefix || selector.startsWith(`${prefix}/`)))) add({
2820
+ const providers = checkoutProvidersFor(selector);
2821
+ const binding = selector === void 0 ? void 0 : providers.at(-1);
2822
+ if (selector === void 0 || unknownCheckoutDestination || binding !== void 0 && binding.source !== "tracked" || hasAmbiguousCheckoutWriters(selector, providers)) add({
2648
2823
  rule: "action-pins",
2649
2824
  line,
2650
2825
  message: "local actions require an unambiguous checkout of the current source snapshot"
@@ -2700,24 +2875,55 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2700
2875
  const checkoutRepository = getInput(node, "repository");
2701
2876
  const checkoutRef = getInput(node, "ref");
2702
2877
  const prefix = checkoutPrefixOf(node);
2878
+ const clean = getInput(node, "clean");
2879
+ const cleanIsResolved = clean === void 0 || isScalar(clean) && !dynamic(clean.value);
2703
2880
  let repo;
2704
2881
  if (checkoutRepository !== void 0) repo = isScalar(checkoutRepository) ? checkoutRepository.value : null;
2705
2882
  const self = repo === void 0 || repo === "${{ github.repository }}" || typeof repo === "string" && repository !== void 0 && repo.toLowerCase() === repository.toLowerCase();
2706
2883
  const workflowSource = repo === "${{ job.workflow_repository }}" && isScalar(checkoutRef) && checkoutRef.value === "${{ job.workflow_sha }}";
2707
- const currentSource = self && checkoutRef === void 0;
2708
- const refValue = isScalar(checkoutRef) ? checkoutRef.value : void 0;
2884
+ const triggers = getNode(document.contents, "on");
2885
+ const callerContext = (() => {
2886
+ if (isMap(triggers)) return [...keysOf(triggers)];
2887
+ if (isSeq(triggers)) return triggers.items.map((event) => isScalar(event) ? event.value : void 0);
2888
+ if (isScalar(triggers)) return [triggers.value];
2889
+ return [];
2890
+ })().includes("workflow_call");
2891
+ let refValue = isScalar(checkoutRef) ? checkoutRef.value : void 0;
2892
+ if (callerContext && (repo === void 0 || repo === "${{ github.repository }}") && checkoutRef === void 0) refValue = "${{ github.sha }}";
2893
+ const currentRef = dynamicRefBody(refValue);
2894
+ const currentSource = self && !callerContext && (checkoutRef === void 0 || currentRef === "github.sha");
2709
2895
  const dynamicRef = dynamicRefBody(refValue);
2710
- 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);
2896
+ const delegatedSource = !currentSource && (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) || callerContext && currentRef === "github.sha");
2897
+ const validSource = cleanIsResolved && ref === approved?.sha && sourceMap({
2898
+ node,
2899
+ key: "if"
2900
+ }) === void 0 && sourceMap({
2901
+ node,
2902
+ key: "continue-on-error"
2903
+ }) === void 0 && (currentSource || workflowSource || delegatedSource);
2904
+ if (!validSource) unreviewableCheckoutSource = true;
2711
2905
  if (!staticRepositoryPath(prefix)) checkoutBindings.push({
2712
2906
  path: ".",
2907
+ dynamicSource: validSource && dynamic(prefix),
2713
2908
  source: "untrusted",
2714
2909
  reason: "checkout destination must be a static repository path",
2715
2910
  line,
2716
- sparse: { mode: "invalid" }
2911
+ sparse: sparseCheckoutOf(node)
2717
2912
  });
2718
2913
  else {
2719
2914
  const destination = normalizeCheckoutPath(prefix);
2720
- const trustedWriter = ref === approved?.sha && sourceMap({
2915
+ checkoutBindings = checkoutBindings.map((entry) => {
2916
+ if (entry.path === destination || destination !== "." && !entry.path.startsWith(`${destination}/`)) return entry;
2917
+ return {
2918
+ path: entry.path,
2919
+ sparse: { mode: "all" },
2920
+ dynamicSource: false,
2921
+ source: "untrusted",
2922
+ reason: "a later ancestor checkout invalidates this descendant destination",
2923
+ line
2924
+ };
2925
+ });
2926
+ const trustedWriter = cleanIsResolved && ref === approved?.sha && sourceMap({
2721
2927
  node,
2722
2928
  key: "if"
2723
2929
  }) === void 0 && sourceMap({
@@ -2726,18 +2932,21 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2726
2932
  }) === void 0 && checkoutDestinations.get(destination) === 1 && !unknownCheckoutDestination;
2727
2933
  if (trustedWriter && delegatedSource && typeof refValue === "string") checkoutBindings.push({
2728
2934
  path: destination,
2935
+ dynamicSource: validSource && delegatedSource,
2729
2936
  sparse: sparseCheckoutOf(node),
2730
2937
  source: "delegated",
2731
2938
  ref: refValue
2732
2939
  });
2733
2940
  else if (trustedWriter && (workflowSource || currentSource)) checkoutBindings.push({
2734
2941
  path: destination,
2942
+ dynamicSource: validSource && delegatedSource,
2735
2943
  sparse: sparseCheckoutOf(node),
2736
2944
  source: "tracked"
2737
2945
  });
2738
2946
  else {
2739
2947
  let reason = "checkout does not select a trusted repository source snapshot";
2740
- if (ref !== approved?.sha) reason = "actions/checkout must use the approved action SHA";
2948
+ if (!cleanIsResolved) reason = "checkout clean must be statically resolved";
2949
+ else if (ref !== approved?.sha) reason = "actions/checkout must use the approved action SHA";
2741
2950
  else if (sourceMap({
2742
2951
  node,
2743
2952
  key: "if"
@@ -2748,6 +2957,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2748
2957
  else if (checkoutDestinations.get(destination) !== 1 || unknownCheckoutDestination) reason = "checkout destination must have exactly one static writer";
2749
2958
  checkoutBindings.push({
2750
2959
  path: destination,
2960
+ dynamicSource: validSource && delegatedSource,
2751
2961
  sparse: sparseCheckoutOf(node),
2752
2962
  source: "untrusted",
2753
2963
  reason,
@@ -2796,21 +3006,23 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2796
3006
  line: reference === void 0 ? line : nodeLine(reference)
2797
3007
  });
2798
3008
  };
2799
- const checkContainer = (node) => {
3009
+ const checkContainer = (node, at) => {
2800
3010
  node = resolveNode(node);
2801
3011
  if (node === void 0) return;
2802
3012
  const image = isMap(node) ? getNode(node, "image") : node;
2803
3013
  checkRuntimeImage({
2804
3014
  image: isScalar(image) ? image.value : void 0,
2805
3015
  line: nodeLine(image ?? node),
2806
- label: "container image"
3016
+ label: "container image",
3017
+ at
2807
3018
  });
2808
3019
  };
2809
- const checkSteps = (node, floor) => {
3020
+ const checkSteps = (node, locationPrefix, floor) => {
2810
3021
  currentEngineFloor = void 0;
2811
3022
  checkoutBindings = [];
2812
3023
  checkoutDestinations = /* @__PURE__ */ new Map();
2813
3024
  unknownCheckoutDestination = false;
3025
+ unreviewableCheckoutSource = false;
2814
3026
  node = resolveNode(node);
2815
3027
  if (!isSeq(node)) return;
2816
3028
  for (const step of node.items) {
@@ -2826,9 +3038,14 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2826
3038
  }
2827
3039
  currentEngineFloor = floor;
2828
3040
  try {
2829
- for (const step of node.items) checkAction(step);
3041
+ for (const step of node.items) {
3042
+ const id = getNode(step, "id");
3043
+ currentLocation = isScalar(id) && typeof id.value === "string" ? `${locationPrefix}.steps.${id.value}` : void 0;
3044
+ checkAction(step);
3045
+ }
2830
3046
  } finally {
2831
3047
  currentEngineFloor = void 0;
3048
+ currentLocation = void 0;
2832
3049
  }
2833
3050
  };
2834
3051
  try {
@@ -2842,15 +3059,15 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2842
3059
  currentEngineFloor = void 0;
2843
3060
  const job = getNode(jobs, key);
2844
3061
  checkAction(job);
2845
- checkContainer(getNode(job, "container"));
3062
+ checkContainer(getNode(job, "container"), `jobs.${key}.container`);
2846
3063
  const services = getNode(job, "services");
2847
- if (isMap(services)) for (const service of keysOf(services)) checkContainer(getNode(services, service));
2848
- checkSteps(getNode(job, "steps"), engineFloors.find((entry) => entry.job === key));
3064
+ if (isMap(services)) for (const service of keysOf(services)) checkContainer(getNode(services, service), `jobs.${key}.services.${service}`);
3065
+ checkSteps(getNode(job, "steps"), `jobs.${key}`, engineFloors.find((entry) => entry.job === key));
2849
3066
  }
2850
3067
  } else {
2851
3068
  const runs = getNode(document.contents, "runs");
2852
3069
  const using = getNode(runs, "using");
2853
- if (isScalar(using) && using.value === "composite") checkSteps(getNode(runs, "steps"));
3070
+ if (isScalar(using) && using.value === "composite") checkSteps(getNode(runs, "steps"), "runs");
2854
3071
  if (isScalar(using) && using.value === "docker") {
2855
3072
  const image = getNode(runs, "image");
2856
3073
  const value = isScalar(image) ? image.value : void 0;
@@ -3079,7 +3296,7 @@ const record = (value) => typeof value === "object" && value !== null && !Array.
3079
3296
  const array = (value) => Array.isArray(value);
3080
3297
  /** Repository-owned declarations are explicit; malformed decisions fail closed. */
3081
3298
  const parseToolchainConfiguration = (input) => {
3082
- if (!record(input) || Object.keys(input).some((key) => key !== "optOuts" && key !== "cloud" && key !== "engineFloors") || input["optOuts"] !== void 0 && !array(input["optOuts"])) throw new Error("stll-toolchain.json accepts only optOuts, cloud and engineFloors declarations");
3299
+ if (!record(input) || Object.keys(input).some((key) => key !== "optOuts" && key !== "cloud" && key !== "engineFloors" && key !== "dynamicSelectors") || input["optOuts"] !== void 0 && !array(input["optOuts"])) throw new Error("stll-toolchain.json accepts only optOuts, cloud, engineFloors and dynamicSelectors declarations");
3083
3300
  const disabled = /* @__PURE__ */ new Set();
3084
3301
  const optOuts = input["optOuts"] ?? [];
3085
3302
  if (!array(optOuts)) throw new Error("optOuts must be an array");
@@ -3092,7 +3309,8 @@ const parseToolchainConfiguration = (input) => {
3092
3309
  return {
3093
3310
  disabled,
3094
3311
  cloud: parseCloudSetup(input["cloud"]),
3095
- engineFloors: parseEngineFloors(input["engineFloors"])
3312
+ engineFloors: parseEngineFloors(input["engineFloors"]),
3313
+ dynamicSelectors: parseDynamicSelectors(input["dynamicSelectors"])
3096
3314
  };
3097
3315
  };
3098
3316
  /** Read one tracked configuration snapshot for guards and policy generation. */
@@ -3158,10 +3376,12 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
3158
3376
  let disabled = /* @__PURE__ */ new Set();
3159
3377
  let cloud;
3160
3378
  const engineFloors = [];
3379
+ let dynamicSelectors = [];
3161
3380
  if (files["stll-toolchain.json"] !== void 0) try {
3162
3381
  const configuration = parseToolchainConfiguration(JSON.parse(files["stll-toolchain.json"]));
3163
3382
  disabled = configuration.disabled;
3164
3383
  cloud = configuration.cloud;
3384
+ dynamicSelectors = configuration.dynamicSelectors;
3165
3385
  for (const entry of configuration.engineFloors) engineFloors.push(resolveEngineFloor(entry, files));
3166
3386
  } catch (error) {
3167
3387
  diagnostics.push({
@@ -3201,6 +3421,7 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
3201
3421
  }
3202
3422
  const exercisedFloors = /* @__PURE__ */ new Set();
3203
3423
  const mismatchedFloors = /* @__PURE__ */ new Set();
3424
+ const selectorMatches = /* @__PURE__ */ new Map();
3204
3425
  diagnostics.push(...checkPackageFiles({
3205
3426
  files,
3206
3427
  policy
@@ -3216,6 +3437,8 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
3216
3437
  }
3217
3438
  },
3218
3439
  trackedFiles,
3440
+ dynamicSelectors: dynamicSelectors.filter((entry) => entry.path === file),
3441
+ onDynamicSelector: (entry) => selectorMatches.set(entry, (selectorMatches.get(entry) ?? 0) + 1),
3219
3442
  repository,
3220
3443
  onDelegated,
3221
3444
  engineFloors: engineFloors.filter((entry) => entry.workflow === file),
@@ -3231,6 +3454,12 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
3231
3454
  line: 1,
3232
3455
  message: `engine floor ${entry.workflow}:${entry.job} requires a setup-node exact patch satisfying ${entry.package} engines.node minimum major ${entry.major}`
3233
3456
  });
3457
+ for (const entry of dynamicSelectors) if (selectorMatches.get(entry) !== 1) diagnostics.push({
3458
+ rule: "configuration",
3459
+ path: "stll-toolchain.json",
3460
+ line: 1,
3461
+ message: `dynamic selector ${entry.path}:${entry.at ?? entry.line} must match exactly one unresolved ${entry.kind} selector`
3462
+ });
3234
3463
  diagnostics.push(...checkDependabot({
3235
3464
  files,
3236
3465
  policy: policy.dependabot