@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.
- package/README.md +84 -10
- package/dist/toolchain-guard.cjs +343 -114
- package/dist/toolchain-guard.cjs.map +1 -1
- package/dist/toolchain-guard.mjs +343 -114
- package/dist/toolchain-guard.mjs.map +1 -1
- package/package.json +1 -1
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,
|
|
@@ -658,7 +669,7 @@ const generateCloudSetup = ({ policy, cloud, nodeVersion }) => {
|
|
|
658
669
|
};
|
|
659
670
|
//#endregion
|
|
660
671
|
//#region src/toolchain-container-inputs.ts
|
|
661
|
-
const record$
|
|
672
|
+
const record$7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
662
673
|
/** Filename class recognized by Dependabot's Docker Compose fetcher. */
|
|
663
674
|
const isComposeDefinitionPath = (file) => /^\.?(?:docker-)?compose(?:-[\w]+)?(?:\.[\w-]+)?\.ya?ml$/i.test(file.split("/").at(-1) ?? "");
|
|
664
675
|
/** Candidate YAML files are classified by their document shape after reading. */
|
|
@@ -705,11 +716,11 @@ const kubernetesPodSpecPaths = {
|
|
|
705
716
|
};
|
|
706
717
|
/** Shared semantic image enumeration for runtime checks and update-root generation. */
|
|
707
718
|
const containerDocumentImages = (document, file) => {
|
|
708
|
-
if (!record$
|
|
719
|
+
if (!record$7(document)) return void 0;
|
|
709
720
|
const services = document["services"];
|
|
710
|
-
if (isComposeDefinitionPath(file) && record$
|
|
721
|
+
if (isComposeDefinitionPath(file) && record$7(services)) {
|
|
711
722
|
const images = [];
|
|
712
|
-
for (const [name, service] of Object.entries(services)) if (record$
|
|
723
|
+
for (const [name, service] of Object.entries(services)) if (record$7(service) && service["image"] !== void 0) images.push({
|
|
713
724
|
image: service["image"],
|
|
714
725
|
path: [
|
|
715
726
|
"services",
|
|
@@ -730,7 +741,7 @@ const containerDocumentImages = (document, file) => {
|
|
|
730
741
|
visited.add(resource);
|
|
731
742
|
if (typeof resource["apiVersion"] !== "string" || typeof resource["kind"] !== "string") return;
|
|
732
743
|
if (resource["kind"] === "List" && Array.isArray(resource["items"])) {
|
|
733
|
-
for (const [index, item] of resource["items"].entries()) if (record$
|
|
744
|
+
for (const [index, item] of resource["items"].entries()) if (record$7(item)) walkResource(item, [
|
|
734
745
|
...prefix,
|
|
735
746
|
"items",
|
|
736
747
|
index
|
|
@@ -740,8 +751,8 @@ const containerDocumentImages = (document, file) => {
|
|
|
740
751
|
const specPath = Object.entries(kubernetesPodSpecPaths).find(([kind]) => kind === resource["kind"])?.[1];
|
|
741
752
|
if (specPath === void 0) return;
|
|
742
753
|
let spec = resource;
|
|
743
|
-
for (const key of specPath) spec = record$
|
|
744
|
-
if (!record$
|
|
754
|
+
for (const key of specPath) spec = record$7(spec) ? spec[key] : void 0;
|
|
755
|
+
if (!record$7(spec)) return;
|
|
745
756
|
for (const key of [
|
|
746
757
|
"containers",
|
|
747
758
|
"initContainers",
|
|
@@ -749,7 +760,7 @@ const containerDocumentImages = (document, file) => {
|
|
|
749
760
|
]) {
|
|
750
761
|
const containers = spec[key];
|
|
751
762
|
if (!Array.isArray(containers)) continue;
|
|
752
|
-
for (const [index, container] of containers.entries()) if (record$
|
|
763
|
+
for (const [index, container] of containers.entries()) if (record$7(container) && container["image"] !== void 0) images.push({
|
|
753
764
|
image: container["image"],
|
|
754
765
|
path: [
|
|
755
766
|
...prefix,
|
|
@@ -852,7 +863,7 @@ const workspaceContains = ({ directory, file, patterns, rootMembership }) => {
|
|
|
852
863
|
//#endregion
|
|
853
864
|
//#region src/toolchain-dependabot.ts
|
|
854
865
|
const dependabotRules = ["dependabot-policy"];
|
|
855
|
-
const record$
|
|
866
|
+
const record$6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
856
867
|
const strings = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
857
868
|
const sameStrings = (value, expected) => {
|
|
858
869
|
if (!strings(value) || value.length !== expected.length) return false;
|
|
@@ -874,8 +885,8 @@ const workspaceTable = (parsed, ecosystem) => {
|
|
|
874
885
|
case "cargo": return parsed["workspace"];
|
|
875
886
|
case "uv": {
|
|
876
887
|
const tool = parsed["tool"];
|
|
877
|
-
const uv = record$
|
|
878
|
-
return record$
|
|
888
|
+
const uv = record$6(tool) ? tool["uv"] : void 0;
|
|
889
|
+
return record$6(uv) ? uv["workspace"] : void 0;
|
|
879
890
|
}
|
|
880
891
|
default: throw new Error("unknown workspace ecosystem", { cause: ecosystem });
|
|
881
892
|
}
|
|
@@ -895,7 +906,7 @@ const workspaceRoots = (files, ecosystem) => {
|
|
|
895
906
|
const document = (0, yaml.parseDocument)(text);
|
|
896
907
|
if (document.errors.length > 0) throw new Error(`${workspaceFile}:1: invalid pnpm workspace YAML`);
|
|
897
908
|
const parsed = document.toJS({ maxAliasCount: 100 });
|
|
898
|
-
if (!record$
|
|
909
|
+
if (!record$6(parsed)) throw new Error(`${workspaceFile}:1: pnpm workspace must contain an object`);
|
|
899
910
|
const patterns = parsed["packages"] ?? [];
|
|
900
911
|
if (!strings(patterns)) throw new Error(`${workspaceFile}:1: pnpm packages must be a string array`);
|
|
901
912
|
workspaces.set(directory, patterns.map((pattern) => pattern.startsWith("!") ? `!${node_path.default.posix.normalize(pattern.slice(1))}` : node_path.default.posix.normalize(pattern)));
|
|
@@ -907,13 +918,13 @@ const workspaceRoots = (files, ecosystem) => {
|
|
|
907
918
|
}
|
|
908
919
|
try {
|
|
909
920
|
const parsed = ecosystem === "npm" ? JSON.parse(files[file] ?? "") : (0, smol_toml.parse)(files[file] ?? "");
|
|
910
|
-
if (!record$
|
|
921
|
+
if (!record$6(parsed)) continue;
|
|
911
922
|
if (ecosystem === "npm" && typeof parsed["packageManager"] === "string" && parsed["packageManager"].startsWith("pnpm@")) continue;
|
|
912
923
|
const value = workspaceTable(parsed, ecosystem);
|
|
913
|
-
const patterns = record$
|
|
914
|
-
const members = ecosystem === "cargo" && record$
|
|
924
|
+
const patterns = record$6(value) ? value[ecosystem === "npm" ? "packages" : "members"] : value;
|
|
925
|
+
const members = ecosystem === "cargo" && record$6(value) && patterns === void 0 ? [] : patterns;
|
|
915
926
|
if (strings(members)) {
|
|
916
|
-
const excluded = ecosystem !== "npm" && record$
|
|
927
|
+
const excluded = ecosystem !== "npm" && record$6(value) && strings(value["exclude"]) ? value["exclude"].map((pattern) => `!${pattern}`) : [];
|
|
917
928
|
workspaces.set(node_path.default.posix.dirname(file), [...members, ...excluded]);
|
|
918
929
|
}
|
|
919
930
|
} catch {}
|
|
@@ -964,11 +975,11 @@ const workspaceRoots = (files, ecosystem) => {
|
|
|
964
975
|
for (const directory of memberDirectories) try {
|
|
965
976
|
const parsed = (0, smol_toml.parse)(files[node_path.default.posix.join(directory, "Cargo.toml")] ?? "");
|
|
966
977
|
const workspace = (0, smol_toml.parse)(files[node_path.default.posix.join(root, "Cargo.toml")] ?? "")["workspace"];
|
|
967
|
-
const inherited = record$
|
|
978
|
+
const inherited = record$6(workspace) ? workspace["dependencies"] : void 0;
|
|
968
979
|
const dependencyOwners = [parsed];
|
|
969
980
|
const targets = parsed["target"];
|
|
970
|
-
if (record$
|
|
971
|
-
for (const target of Object.values(targets)) if (record$
|
|
981
|
+
if (record$6(targets)) {
|
|
982
|
+
for (const target of Object.values(targets)) if (record$6(target)) dependencyOwners.push(target);
|
|
972
983
|
}
|
|
973
984
|
for (const owner of dependencyOwners) for (const key of [
|
|
974
985
|
"dependencies",
|
|
@@ -976,12 +987,12 @@ const workspaceRoots = (files, ecosystem) => {
|
|
|
976
987
|
"build-dependencies"
|
|
977
988
|
]) {
|
|
978
989
|
const dependencies = owner[key];
|
|
979
|
-
if (!record$
|
|
990
|
+
if (!record$6(dependencies)) continue;
|
|
980
991
|
for (const [name, dependency] of Object.entries(dependencies)) {
|
|
981
|
-
if (!record$
|
|
992
|
+
if (!record$6(dependency)) continue;
|
|
982
993
|
const isInherited = dependency["workspace"] === true;
|
|
983
|
-
const resolved = isInherited && record$
|
|
984
|
-
if (!record$
|
|
994
|
+
const resolved = isInherited && record$6(inherited) ? inherited[name] : dependency;
|
|
995
|
+
if (!record$6(resolved) || typeof resolved["path"] !== "string") continue;
|
|
985
996
|
if (node_path.default.posix.isAbsolute(resolved["path"]) || /\\|\$|^[A-Za-z]:/.test(resolved["path"])) continue;
|
|
986
997
|
const target = node_path.default.posix.normalize(node_path.default.posix.join(isInherited ? root : directory, resolved["path"]));
|
|
987
998
|
const relative = node_path.default.posix.relative(root, target);
|
|
@@ -1009,7 +1020,7 @@ const javascriptEcosystem = (directory, files) => {
|
|
|
1009
1020
|
if (files[node_path.default.posix.join(root, "bun.lock")] !== void 0) return "bun";
|
|
1010
1021
|
try {
|
|
1011
1022
|
const manifest = JSON.parse(files[node_path.default.posix.join(root, "package.json")] ?? "{}");
|
|
1012
|
-
if (record$
|
|
1023
|
+
if (record$6(manifest) && typeof manifest["packageManager"] === "string" && manifest["packageManager"].startsWith("bun@")) return "bun";
|
|
1013
1024
|
} catch {}
|
|
1014
1025
|
return "npm";
|
|
1015
1026
|
};
|
|
@@ -1021,7 +1032,7 @@ const pythonEcosystem = (file, files) => {
|
|
|
1021
1032
|
const pyproject = files[node_path.default.posix.join(root, "pyproject.toml")];
|
|
1022
1033
|
if (pyproject !== void 0) try {
|
|
1023
1034
|
const tool = (0, smol_toml.parse)(pyproject)["tool"];
|
|
1024
|
-
if (record$
|
|
1035
|
+
if (record$6(tool) && record$6(tool["uv"])) return "uv";
|
|
1025
1036
|
} catch {}
|
|
1026
1037
|
return "pip";
|
|
1027
1038
|
};
|
|
@@ -1092,7 +1103,7 @@ const checkDependabot = ({ files, policy }) => {
|
|
|
1092
1103
|
add("cannot resolve Dependabot YAML aliases");
|
|
1093
1104
|
return diagnostics;
|
|
1094
1105
|
}
|
|
1095
|
-
if (!record$
|
|
1106
|
+
if (!record$6(parsed) || parsed["version"] !== 2 || !Array.isArray(parsed["updates"])) {
|
|
1096
1107
|
add("Dependabot must declare version: 2 and an updates array");
|
|
1097
1108
|
return diagnostics;
|
|
1098
1109
|
}
|
|
@@ -1105,7 +1116,7 @@ const checkDependabot = ({ files, policy }) => {
|
|
|
1105
1116
|
});
|
|
1106
1117
|
if (legacyDocument.errors.length === 0) legacy = legacyDocument.toJS({ maxAliasCount: 100 });
|
|
1107
1118
|
} catch {}
|
|
1108
|
-
const legacyUpdates = record$
|
|
1119
|
+
const legacyUpdates = record$6(legacy) && Array.isArray(legacy["updates"]) ? legacy["updates"] : [];
|
|
1109
1120
|
const rootClaims = /* @__PURE__ */ new Map();
|
|
1110
1121
|
const directoryClaims = /* @__PURE__ */ new Map();
|
|
1111
1122
|
for (const [index, update] of parsed["updates"].entries()) {
|
|
@@ -1119,7 +1130,7 @@ const checkDependabot = ({ files, policy }) => {
|
|
|
1119
1130
|
return offset === void 0 ? 1 : lineCounter.linePos(offset).line;
|
|
1120
1131
|
};
|
|
1121
1132
|
const report = (key, message) => add(message, lineOf(key));
|
|
1122
|
-
if (!record$
|
|
1133
|
+
if (!record$6(update) || typeof update["package-ecosystem"] !== "string") {
|
|
1123
1134
|
report("package-ecosystem", "each Dependabot update must declare a package ecosystem");
|
|
1124
1135
|
continue;
|
|
1125
1136
|
}
|
|
@@ -1153,26 +1164,75 @@ const checkDependabot = ({ files, policy }) => {
|
|
|
1153
1164
|
covered.set(ecosystem, matches);
|
|
1154
1165
|
const schedule = update["schedule"];
|
|
1155
1166
|
const legacyUpdate = legacyUpdates.at(index);
|
|
1156
|
-
const legacySchedule = record$
|
|
1157
|
-
if (record$
|
|
1158
|
-
if (!record$
|
|
1167
|
+
const legacySchedule = record$6(legacyUpdate) ? legacyUpdate["schedule"] : void 0;
|
|
1168
|
+
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`);
|
|
1169
|
+
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`);
|
|
1159
1170
|
const cooldown = update["cooldown"];
|
|
1160
|
-
if (!record$
|
|
1171
|
+
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}`);
|
|
1161
1172
|
const groups = update["groups"];
|
|
1162
|
-
if (!record$
|
|
1173
|
+
if (!record$6(groups) || Object.keys(groups).length !== Object.keys(policy.groups).length || Object.entries(policy.groups).some(([name, expected]) => {
|
|
1163
1174
|
const group = groups[name];
|
|
1164
|
-
return !record$
|
|
1175
|
+
return !record$6(group) || Object.keys(group).length !== 2 || !sameStrings(group["patterns"], expected.patterns) || !sameStrings(group["update-types"], expected.updateTypes);
|
|
1165
1176
|
})) report("groups", `${ecosystem}: groups must match the shared policy`);
|
|
1166
1177
|
const ignored = ignoredFor(ecosystem, policy);
|
|
1167
1178
|
const ignore = update["ignore"];
|
|
1168
1179
|
if (ignore !== void 0 && (!Array.isArray(ignore) || ignore.length === 0)) report("ignore", `${ecosystem}: omit ignore or provide a nonempty array`);
|
|
1169
|
-
if (Array.isArray(ignore) && ignore.some((entry) => !record$
|
|
1170
|
-
for (const name of ignored) if (!Array.isArray(ignore) || !ignore.some((entry) => record$
|
|
1180
|
+
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`);
|
|
1181
|
+
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}`);
|
|
1171
1182
|
}
|
|
1172
1183
|
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}`);
|
|
1173
1184
|
return diagnostics;
|
|
1174
1185
|
};
|
|
1175
1186
|
//#endregion
|
|
1187
|
+
//#region src/toolchain-dynamic-selectors.ts
|
|
1188
|
+
const locatorValidators = {
|
|
1189
|
+
image: /^(?:jobs\.[\w-]+\.(?:container|services\.[\w-]+)|services\.[\w.-]+\.image)$/,
|
|
1190
|
+
"bun-source": /^(?:jobs\.[\w-]+|runs)\.steps\.[\w-]+$/
|
|
1191
|
+
};
|
|
1192
|
+
const knownKind = (value) => typeof value === "string" && Object.hasOwn(locatorValidators, value);
|
|
1193
|
+
const record$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1194
|
+
/** One reviewed declaration identifies one unresolved selector, never a rule. */
|
|
1195
|
+
const parseDynamicSelectors = (input) => {
|
|
1196
|
+
if (input === void 0) return [];
|
|
1197
|
+
if (!Array.isArray(input)) throw new Error("dynamicSelectors must be an array");
|
|
1198
|
+
const result = [];
|
|
1199
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1200
|
+
for (const entry of input) {
|
|
1201
|
+
if (!record$5(entry) || Object.keys(entry).some((key) => ![
|
|
1202
|
+
"path",
|
|
1203
|
+
"kind",
|
|
1204
|
+
"reason",
|
|
1205
|
+
"at",
|
|
1206
|
+
"line"
|
|
1207
|
+
].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");
|
|
1208
|
+
const kind = entry["kind"];
|
|
1209
|
+
if (!knownKind(kind)) throw new Error("unknown dynamic selector kind");
|
|
1210
|
+
let selector;
|
|
1211
|
+
if (Object.hasOwn(entry, "line") && !Object.hasOwn(entry, "at") && typeof entry["line"] === "number" && Number.isSafeInteger(entry["line"]) && entry["line"] > 0) selector = {
|
|
1212
|
+
path: entry["path"],
|
|
1213
|
+
kind,
|
|
1214
|
+
reason: entry["reason"],
|
|
1215
|
+
line: entry["line"]
|
|
1216
|
+
};
|
|
1217
|
+
else if (!Object.hasOwn(entry, "line") && typeof entry["at"] === "string" && locatorValidators[kind].test(entry["at"])) selector = {
|
|
1218
|
+
path: entry["path"],
|
|
1219
|
+
kind,
|
|
1220
|
+
reason: entry["reason"],
|
|
1221
|
+
at: entry["at"]
|
|
1222
|
+
};
|
|
1223
|
+
else throw new Error("dynamic selector requires one positive line or a kind-specific job/service/step ID locator");
|
|
1224
|
+
const key = JSON.stringify([
|
|
1225
|
+
selector.path,
|
|
1226
|
+
selector.kind,
|
|
1227
|
+
selector.at ?? selector.line
|
|
1228
|
+
]);
|
|
1229
|
+
if (keys.has(key)) throw new Error("dynamic selector locations must be unique");
|
|
1230
|
+
keys.add(key);
|
|
1231
|
+
result.push(selector);
|
|
1232
|
+
}
|
|
1233
|
+
return result;
|
|
1234
|
+
};
|
|
1235
|
+
//#endregion
|
|
1176
1236
|
//#region src/toolchain-engine-floors.ts
|
|
1177
1237
|
const record$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1178
1238
|
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 !== "..");
|
|
@@ -1984,7 +2044,7 @@ const pythonRequirementMatches = (requirement, version) => {
|
|
|
1984
2044
|
});
|
|
1985
2045
|
};
|
|
1986
2046
|
/** Check runtime declarations only in the files that declare or configure them. */
|
|
1987
|
-
const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, repository, onDelegated, engineFloors = [], onEngineFloor }) => {
|
|
2047
|
+
const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, repository, onDelegated, dynamicSelectors = [], onDynamicSelector, engineFloors = [], onEngineFloor }) => {
|
|
1988
2048
|
const diagnostics = [];
|
|
1989
2049
|
const name = node_path.default.posix.basename(file);
|
|
1990
2050
|
const lines = text.split(/\r?\n/);
|
|
@@ -1996,6 +2056,16 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
1996
2056
|
message
|
|
1997
2057
|
});
|
|
1998
2058
|
};
|
|
2059
|
+
let currentLocation;
|
|
2060
|
+
const dynamic = (value) => typeof value === "string" && value.includes("$");
|
|
2061
|
+
const authorizeDynamic = (kind, line, at = currentLocation) => {
|
|
2062
|
+
const matches = dynamicSelectors.filter((entry) => entry.path === file && entry.kind === kind && (entry.at !== void 0 ? entry.at === at : entry.line === line));
|
|
2063
|
+
if (matches.length !== 1) return false;
|
|
2064
|
+
const entry = matches.at(0);
|
|
2065
|
+
if (entry === void 0) return false;
|
|
2066
|
+
onDynamicSelector?.(entry);
|
|
2067
|
+
return true;
|
|
2068
|
+
};
|
|
1999
2069
|
const lineOf = (key) => Math.max(1, lines.findIndex((line) => line.includes(key)) + 1);
|
|
2000
2070
|
const pin = ({ rule, value, expected, label, line = 1, selector }) => {
|
|
2001
2071
|
let matches = value === expected;
|
|
@@ -2197,32 +2267,73 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2197
2267
|
line: index + 1
|
|
2198
2268
|
});
|
|
2199
2269
|
});
|
|
2200
|
-
const checkRuntimeImage = ({ image, line, label }) => {
|
|
2201
|
-
|
|
2270
|
+
const checkRuntimeImage = ({ image, line, label, at }) => {
|
|
2271
|
+
const unknownRuntime = () => {
|
|
2272
|
+
if (dynamic(image) && authorizeDynamic("image", line, at)) return;
|
|
2202
2273
|
add({
|
|
2203
2274
|
rule: "runtime-docker",
|
|
2204
2275
|
line,
|
|
2205
|
-
message: `cannot determine ${label} runtime; use a static image or a
|
|
2276
|
+
message: `cannot determine ${label} runtime; use a static image or a scoped dynamicSelectors declaration`
|
|
2206
2277
|
});
|
|
2278
|
+
};
|
|
2279
|
+
if (typeof image !== "string") {
|
|
2280
|
+
unknownRuntime();
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
const canonical = canonicalDockerRuntime(image);
|
|
2284
|
+
const structuralImage = canonical.replace(/\$\{[^}]*\}|\$[A-Za-z_][A-Za-z_\d]*/g, (variable) => "x".repeat(variable.length));
|
|
2285
|
+
const digestSeparator = structuralImage.indexOf("@");
|
|
2286
|
+
const reference = digestSeparator < 0 ? canonical : canonical.slice(0, digestSeparator);
|
|
2287
|
+
const structuralReference = digestSeparator < 0 ? structuralImage : structuralImage.slice(0, digestSeparator);
|
|
2288
|
+
const managedFamily = /^(node|python|oven\/bun)(?=:|$)/i.exec(structuralReference)?.[1];
|
|
2289
|
+
const tagSeparator = managedFamily === void 0 ? structuralReference.lastIndexOf(":") : structuralReference.indexOf(":", managedFamily.length);
|
|
2290
|
+
const hasTag = managedFamily === void 0 ? tagSeparator > structuralReference.lastIndexOf("/") : tagSeparator !== -1;
|
|
2291
|
+
const imageRepository = hasTag ? reference.slice(0, tagSeparator) : reference;
|
|
2292
|
+
if (imageRepository.length === 0 || imageRepository.includes("$")) {
|
|
2293
|
+
unknownRuntime();
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
if (!/^(node|python|oven\/bun)$/i.test(imageRepository)) return;
|
|
2297
|
+
const tool = imageRepository.toLowerCase();
|
|
2298
|
+
if (image.includes("$")) {
|
|
2299
|
+
unknownRuntime();
|
|
2207
2300
|
return;
|
|
2208
2301
|
}
|
|
2209
|
-
const
|
|
2210
|
-
if (
|
|
2211
|
-
const tool = runtime[1]?.toLowerCase();
|
|
2212
|
-
if (image.includes("@")) {
|
|
2302
|
+
const digest = digestSeparator < 0 ? void 0 : canonical.slice(digestSeparator + 1);
|
|
2303
|
+
if (digest !== void 0 && !/^sha256:[a-fA-F\d]{64}$/.test(digest)) {
|
|
2213
2304
|
add({
|
|
2214
2305
|
rule: tool === "oven/bun" ? "bun-pins" : "runtime-docker",
|
|
2215
2306
|
line,
|
|
2216
|
-
message: `${label} ${tool} digest
|
|
2307
|
+
message: `${label} ${tool} digest must be sha256 followed by 64 hexadecimal characters`
|
|
2217
2308
|
});
|
|
2218
2309
|
return;
|
|
2219
2310
|
}
|
|
2220
|
-
const tag =
|
|
2311
|
+
const tag = hasTag ? reference.slice(tagSeparator + 1) : void 0;
|
|
2221
2312
|
const variantSuffix = tool === "oven/bun" ? /-(?:alpine|slim|debian)$/ : /-(?:alpine(?:\d+(?:\.\d+)*)?|(?:bookworm|bullseye|trixie|buster)(?:-slim)?|slim)$/;
|
|
2222
2313
|
const version = tag?.replace(variantSuffix, "");
|
|
2223
2314
|
let expected = policy.bun;
|
|
2224
2315
|
if (tool === "node") expected = policy.node;
|
|
2225
2316
|
else if (tool === "python") expected = policy.python;
|
|
2317
|
+
if (digest !== void 0) {
|
|
2318
|
+
if (version === void 0 || !/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/.test(version)) {
|
|
2319
|
+
add({
|
|
2320
|
+
rule: tool === "oven/bun" ? "bun-pins" : "runtime-docker",
|
|
2321
|
+
line,
|
|
2322
|
+
message: `${label} ${tool} digest requires an exact stable patch tag matching ${expected}`
|
|
2323
|
+
});
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
if (tool === "node" && trackedFiles.has(".node-version")) {
|
|
2327
|
+
if (readFile(".node-version")?.trim() !== version) {
|
|
2328
|
+
add({
|
|
2329
|
+
rule: "runtime-docker",
|
|
2330
|
+
line,
|
|
2331
|
+
message: `${label} node digest tag must match tracked .node-version, found ${version}`
|
|
2332
|
+
});
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2226
2337
|
pin({
|
|
2227
2338
|
rule: tool === "oven/bun" ? "bun-pins" : "runtime-docker",
|
|
2228
2339
|
value: version,
|
|
@@ -2335,14 +2446,6 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2335
2446
|
if (from === null || from[1] === void 0) return;
|
|
2336
2447
|
argumentScope = "stage";
|
|
2337
2448
|
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
2449
|
const stage = from[2]?.toLowerCase();
|
|
2347
2450
|
const previousStage = stages.has(image.toLowerCase());
|
|
2348
2451
|
if (stage !== void 0) stages.add(stage);
|
|
@@ -2399,6 +2502,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2399
2502
|
checkRuntimeImage({
|
|
2400
2503
|
image: entry.image,
|
|
2401
2504
|
line,
|
|
2505
|
+
at: entry.path.join("."),
|
|
2402
2506
|
label: classified?.ecosystem === "docker-compose" ? "Compose image" : "Kubernetes image"
|
|
2403
2507
|
});
|
|
2404
2508
|
}
|
|
@@ -2433,8 +2537,22 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2433
2537
|
let checkoutBindings = [];
|
|
2434
2538
|
let checkoutDestinations = /* @__PURE__ */ new Map();
|
|
2435
2539
|
let unknownCheckoutDestination = false;
|
|
2540
|
+
let unreviewableCheckoutSource = false;
|
|
2436
2541
|
const staticRepositoryPath = (value) => typeof value === "string" && value !== "" && !value.includes("${{") && !value.includes("\\") && !value.includes(":") && !node_path.default.posix.isAbsolute(value) && !value.split("/").includes("..");
|
|
2437
2542
|
const normalizeCheckoutPath = (prefix) => node_path.default.posix.normalize(prefix).replace(/\/$/, "") || ".";
|
|
2543
|
+
const checkoutProvidersFor = (selector) => {
|
|
2544
|
+
if (selector === void 0) return checkoutBindings;
|
|
2545
|
+
let deepest;
|
|
2546
|
+
for (const entry of checkoutBindings) {
|
|
2547
|
+
if (entry.path !== "." && selector !== entry.path && !selector.startsWith(`${entry.path}/`)) continue;
|
|
2548
|
+
if (deepest === void 0 || entry.path !== "." && (deepest.path === "." || entry.path.length > deepest.path.length)) deepest = entry;
|
|
2549
|
+
}
|
|
2550
|
+
return checkoutBindings.filter((entry) => entry.path === deepest?.path);
|
|
2551
|
+
};
|
|
2552
|
+
const hasAmbiguousCheckoutWriters = (selector, providers) => {
|
|
2553
|
+
if (providers.length > 0) return providers.some((entry) => (checkoutDestinations.get(entry.path) ?? 0) > 1);
|
|
2554
|
+
return [...checkoutDestinations].some(([prefix, count]) => count > 1 && (selector === void 0 || prefix === "." || selector === prefix || selector.startsWith(`${prefix}/`)));
|
|
2555
|
+
};
|
|
2438
2556
|
const checkReference = ({ value, tool, line }) => {
|
|
2439
2557
|
const allowed = {
|
|
2440
2558
|
node: [".node-version", ".nvmrc"],
|
|
@@ -2443,8 +2561,28 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2443
2561
|
}[tool];
|
|
2444
2562
|
const rule = tool === "bun" ? "bun-pins" : "runtime-workflow";
|
|
2445
2563
|
const selector = staticRepositoryPath(value) ? node_path.default.posix.normalize(value) : void 0;
|
|
2446
|
-
const
|
|
2564
|
+
const providers = checkoutProvidersFor(selector);
|
|
2565
|
+
const binding = selector === void 0 ? void 0 : providers.at(-1);
|
|
2566
|
+
const dynamicSource = providers.some((entry) => entry.dynamicSource);
|
|
2567
|
+
const sparseFailure = providers.find((entry) => {
|
|
2568
|
+
if (entry.sparse.mode === "invalid") return true;
|
|
2569
|
+
if (entry.sparse.mode !== "files" && entry.sparse.mode !== "mixed") return false;
|
|
2570
|
+
if (selector === void 0) return true;
|
|
2571
|
+
const selectedFile = entry.path === "." ? selector : node_path.default.posix.relative(entry.path, selector);
|
|
2572
|
+
return !entry.sparse.paths.has(selectedFile);
|
|
2573
|
+
});
|
|
2574
|
+
if (sparseFailure !== void 0) {
|
|
2575
|
+
let missingFile = "the selected version file";
|
|
2576
|
+
if (selector !== void 0) missingFile = sparseFailure.path === "." ? selector : node_path.default.posix.relative(sparseFailure.path, selector);
|
|
2577
|
+
add({
|
|
2578
|
+
rule,
|
|
2579
|
+
line,
|
|
2580
|
+
message: `checkout sparse-checkout must explicitly list ${missingFile} without invalid literal patterns`
|
|
2581
|
+
});
|
|
2582
|
+
return;
|
|
2583
|
+
}
|
|
2447
2584
|
if (selector === void 0) {
|
|
2585
|
+
if (tool === "bun" && dynamic(value) && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
|
|
2448
2586
|
add({
|
|
2449
2587
|
rule,
|
|
2450
2588
|
line,
|
|
@@ -2452,7 +2590,8 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2452
2590
|
});
|
|
2453
2591
|
return;
|
|
2454
2592
|
}
|
|
2455
|
-
if (unknownCheckoutDestination ||
|
|
2593
|
+
if (unknownCheckoutDestination || hasAmbiguousCheckoutWriters(selector, providers)) {
|
|
2594
|
+
if (tool === "bun" && dynamicSource && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
|
|
2456
2595
|
add({
|
|
2457
2596
|
rule,
|
|
2458
2597
|
line,
|
|
@@ -2461,6 +2600,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2461
2600
|
return;
|
|
2462
2601
|
}
|
|
2463
2602
|
if (binding?.source === "untrusted") {
|
|
2603
|
+
if (tool === "bun" && dynamicSource && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
|
|
2464
2604
|
add({
|
|
2465
2605
|
rule,
|
|
2466
2606
|
line: binding.line,
|
|
@@ -2477,13 +2617,15 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2477
2617
|
});
|
|
2478
2618
|
return;
|
|
2479
2619
|
}
|
|
2480
|
-
if (binding !== void 0 && (binding.sparse.mode === "
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2620
|
+
if (binding !== void 0 && (binding.sparse.mode === "dynamic" || binding.sparse.mode === "mixed")) {
|
|
2621
|
+
if (tool !== "bun" || unreviewableCheckoutSource || !authorizeDynamic("bun-source", line)) {
|
|
2622
|
+
add({
|
|
2623
|
+
rule,
|
|
2624
|
+
line,
|
|
2625
|
+
message: `checkout sparse-checkout must explicitly list ${target} without dynamic, glob, or negation patterns`
|
|
2626
|
+
});
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2487
2629
|
}
|
|
2488
2630
|
if (binding?.source === "delegated") {
|
|
2489
2631
|
onDelegated?.({
|
|
@@ -2605,10 +2747,10 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2605
2747
|
return inputs;
|
|
2606
2748
|
};
|
|
2607
2749
|
const getInput = (node, key) => inputsOf(node).get(key.toUpperCase());
|
|
2608
|
-
const
|
|
2609
|
-
if (
|
|
2750
|
+
const expressionAt = (value, start) => {
|
|
2751
|
+
if (!value.startsWith("${{", start)) return void 0;
|
|
2610
2752
|
let quote;
|
|
2611
|
-
for (let index = 3; index < value.length; index++) {
|
|
2753
|
+
for (let index = start + 3; index < value.length; index++) {
|
|
2612
2754
|
const character = value.at(index);
|
|
2613
2755
|
if (quote !== void 0) {
|
|
2614
2756
|
if (character === quote) if (quote === "'" && value.at(index + 1) === "'") index++;
|
|
@@ -2618,9 +2760,33 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2618
2760
|
}
|
|
2619
2761
|
if (character === "'" || character === "\"") quote = character;
|
|
2620
2762
|
else if (value.startsWith("${{", index)) return void 0;
|
|
2621
|
-
else if (character === "}" && value.at(index + 1) === "}") return
|
|
2763
|
+
else if (character === "}" && value.at(index + 1) === "}") return {
|
|
2764
|
+
body: value.slice(start + 3, index).trim(),
|
|
2765
|
+
end: index + 2
|
|
2766
|
+
};
|
|
2622
2767
|
}
|
|
2623
2768
|
};
|
|
2769
|
+
const dynamicRefBody = (value) => {
|
|
2770
|
+
if (typeof value !== "string") return void 0;
|
|
2771
|
+
const expression = expressionAt(value, 0);
|
|
2772
|
+
return expression?.end === value.length ? expression.body : void 0;
|
|
2773
|
+
};
|
|
2774
|
+
const sparseEntryIsValid = (entry) => {
|
|
2775
|
+
let literalPath = "";
|
|
2776
|
+
let position = 0;
|
|
2777
|
+
while (position < entry.length) {
|
|
2778
|
+
const start = entry.indexOf("${{", position);
|
|
2779
|
+
const literal = entry.slice(position, start < 0 ? entry.length : start);
|
|
2780
|
+
if (/[!$*?[\]\\:]/.test(literal) || literal.split("/").includes("..")) return false;
|
|
2781
|
+
literalPath += literal;
|
|
2782
|
+
if (start < 0) break;
|
|
2783
|
+
const expression = expressionAt(entry, start);
|
|
2784
|
+
if (expression === void 0 || expression.body === "") return false;
|
|
2785
|
+
literalPath += "expression";
|
|
2786
|
+
position = expression.end;
|
|
2787
|
+
}
|
|
2788
|
+
return staticRepositoryPath(literalPath);
|
|
2789
|
+
};
|
|
2624
2790
|
const checkoutPrefixOf = (node) => {
|
|
2625
2791
|
const destination = getInput(node, "path");
|
|
2626
2792
|
if (destination === void 0) return ".";
|
|
@@ -2633,11 +2799,19 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2633
2799
|
const value = getInput(node, "sparse-checkout");
|
|
2634
2800
|
const cone = getInput(node, "sparse-checkout-cone-mode");
|
|
2635
2801
|
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" };
|
|
2636
|
-
const paths = value.value.split(/\r?\n/).map((entry) => entry.trim()).filter((entry) => entry !== "");
|
|
2637
|
-
if (paths.length === 0 || paths.some((entry) => !
|
|
2802
|
+
const paths = value.value.split(/\r?\n/).map((entry) => entry.trim().replace(/^\//, "")).filter((entry) => entry !== "");
|
|
2803
|
+
if (paths.length === 0 || paths.some((entry) => !sparseEntryIsValid(entry))) return { mode: "invalid" };
|
|
2804
|
+
const literalPaths = new Set(paths.filter((entry) => !dynamic(entry)));
|
|
2805
|
+
if (paths.some(dynamic)) {
|
|
2806
|
+
if (literalPaths.size === 0) return { mode: "dynamic" };
|
|
2807
|
+
return {
|
|
2808
|
+
mode: "mixed",
|
|
2809
|
+
paths: literalPaths
|
|
2810
|
+
};
|
|
2811
|
+
}
|
|
2638
2812
|
return {
|
|
2639
2813
|
mode: "files",
|
|
2640
|
-
paths:
|
|
2814
|
+
paths: literalPaths
|
|
2641
2815
|
};
|
|
2642
2816
|
};
|
|
2643
2817
|
let currentEngineFloor;
|
|
@@ -2667,8 +2841,9 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2667
2841
|
}
|
|
2668
2842
|
if (value.startsWith("./")) {
|
|
2669
2843
|
const selector = staticRepositoryPath(value) ? node_path.default.posix.normalize(value) : void 0;
|
|
2670
|
-
const
|
|
2671
|
-
|
|
2844
|
+
const providers = checkoutProvidersFor(selector);
|
|
2845
|
+
const binding = selector === void 0 ? void 0 : providers.at(-1);
|
|
2846
|
+
if (selector === void 0 || unknownCheckoutDestination || binding !== void 0 && binding.source !== "tracked" || hasAmbiguousCheckoutWriters(selector, providers)) add({
|
|
2672
2847
|
rule: "action-pins",
|
|
2673
2848
|
line,
|
|
2674
2849
|
message: "local actions require an unambiguous checkout of the current source snapshot"
|
|
@@ -2724,24 +2899,55 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2724
2899
|
const checkoutRepository = getInput(node, "repository");
|
|
2725
2900
|
const checkoutRef = getInput(node, "ref");
|
|
2726
2901
|
const prefix = checkoutPrefixOf(node);
|
|
2902
|
+
const clean = getInput(node, "clean");
|
|
2903
|
+
const cleanIsResolved = clean === void 0 || (0, yaml.isScalar)(clean) && !dynamic(clean.value);
|
|
2727
2904
|
let repo;
|
|
2728
2905
|
if (checkoutRepository !== void 0) repo = (0, yaml.isScalar)(checkoutRepository) ? checkoutRepository.value : null;
|
|
2729
2906
|
const self = repo === void 0 || repo === "${{ github.repository }}" || typeof repo === "string" && repository !== void 0 && repo.toLowerCase() === repository.toLowerCase();
|
|
2730
2907
|
const workflowSource = repo === "${{ job.workflow_repository }}" && (0, yaml.isScalar)(checkoutRef) && checkoutRef.value === "${{ job.workflow_sha }}";
|
|
2731
|
-
const
|
|
2732
|
-
const
|
|
2908
|
+
const triggers = getNode(document.contents, "on");
|
|
2909
|
+
const callerContext = (() => {
|
|
2910
|
+
if ((0, yaml.isMap)(triggers)) return [...keysOf(triggers)];
|
|
2911
|
+
if ((0, yaml.isSeq)(triggers)) return triggers.items.map((event) => (0, yaml.isScalar)(event) ? event.value : void 0);
|
|
2912
|
+
if ((0, yaml.isScalar)(triggers)) return [triggers.value];
|
|
2913
|
+
return [];
|
|
2914
|
+
})().includes("workflow_call");
|
|
2915
|
+
let refValue = (0, yaml.isScalar)(checkoutRef) ? checkoutRef.value : void 0;
|
|
2916
|
+
if (callerContext && (repo === void 0 || repo === "${{ github.repository }}") && checkoutRef === void 0) refValue = "${{ github.sha }}";
|
|
2917
|
+
const currentRef = dynamicRefBody(refValue);
|
|
2918
|
+
const currentSource = self && !callerContext && (checkoutRef === void 0 || currentRef === "github.sha");
|
|
2733
2919
|
const dynamicRef = dynamicRefBody(refValue);
|
|
2734
|
-
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);
|
|
2920
|
+
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");
|
|
2921
|
+
const validSource = cleanIsResolved && ref === approved?.sha && sourceMap({
|
|
2922
|
+
node,
|
|
2923
|
+
key: "if"
|
|
2924
|
+
}) === void 0 && sourceMap({
|
|
2925
|
+
node,
|
|
2926
|
+
key: "continue-on-error"
|
|
2927
|
+
}) === void 0 && (currentSource || workflowSource || delegatedSource);
|
|
2928
|
+
if (!validSource) unreviewableCheckoutSource = true;
|
|
2735
2929
|
if (!staticRepositoryPath(prefix)) checkoutBindings.push({
|
|
2736
2930
|
path: ".",
|
|
2931
|
+
dynamicSource: validSource && dynamic(prefix),
|
|
2737
2932
|
source: "untrusted",
|
|
2738
2933
|
reason: "checkout destination must be a static repository path",
|
|
2739
2934
|
line,
|
|
2740
|
-
sparse:
|
|
2935
|
+
sparse: sparseCheckoutOf(node)
|
|
2741
2936
|
});
|
|
2742
2937
|
else {
|
|
2743
2938
|
const destination = normalizeCheckoutPath(prefix);
|
|
2744
|
-
|
|
2939
|
+
checkoutBindings = checkoutBindings.map((entry) => {
|
|
2940
|
+
if (entry.path === destination || destination !== "." && !entry.path.startsWith(`${destination}/`)) return entry;
|
|
2941
|
+
return {
|
|
2942
|
+
path: entry.path,
|
|
2943
|
+
sparse: { mode: "all" },
|
|
2944
|
+
dynamicSource: false,
|
|
2945
|
+
source: "untrusted",
|
|
2946
|
+
reason: "a later ancestor checkout invalidates this descendant destination",
|
|
2947
|
+
line
|
|
2948
|
+
};
|
|
2949
|
+
});
|
|
2950
|
+
const trustedWriter = cleanIsResolved && ref === approved?.sha && sourceMap({
|
|
2745
2951
|
node,
|
|
2746
2952
|
key: "if"
|
|
2747
2953
|
}) === void 0 && sourceMap({
|
|
@@ -2750,18 +2956,21 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2750
2956
|
}) === void 0 && checkoutDestinations.get(destination) === 1 && !unknownCheckoutDestination;
|
|
2751
2957
|
if (trustedWriter && delegatedSource && typeof refValue === "string") checkoutBindings.push({
|
|
2752
2958
|
path: destination,
|
|
2959
|
+
dynamicSource: validSource && delegatedSource,
|
|
2753
2960
|
sparse: sparseCheckoutOf(node),
|
|
2754
2961
|
source: "delegated",
|
|
2755
2962
|
ref: refValue
|
|
2756
2963
|
});
|
|
2757
2964
|
else if (trustedWriter && (workflowSource || currentSource)) checkoutBindings.push({
|
|
2758
2965
|
path: destination,
|
|
2966
|
+
dynamicSource: validSource && delegatedSource,
|
|
2759
2967
|
sparse: sparseCheckoutOf(node),
|
|
2760
2968
|
source: "tracked"
|
|
2761
2969
|
});
|
|
2762
2970
|
else {
|
|
2763
2971
|
let reason = "checkout does not select a trusted repository source snapshot";
|
|
2764
|
-
if (
|
|
2972
|
+
if (!cleanIsResolved) reason = "checkout clean must be statically resolved";
|
|
2973
|
+
else if (ref !== approved?.sha) reason = "actions/checkout must use the approved action SHA";
|
|
2765
2974
|
else if (sourceMap({
|
|
2766
2975
|
node,
|
|
2767
2976
|
key: "if"
|
|
@@ -2772,6 +2981,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2772
2981
|
else if (checkoutDestinations.get(destination) !== 1 || unknownCheckoutDestination) reason = "checkout destination must have exactly one static writer";
|
|
2773
2982
|
checkoutBindings.push({
|
|
2774
2983
|
path: destination,
|
|
2984
|
+
dynamicSource: validSource && delegatedSource,
|
|
2775
2985
|
sparse: sparseCheckoutOf(node),
|
|
2776
2986
|
source: "untrusted",
|
|
2777
2987
|
reason,
|
|
@@ -2820,21 +3030,23 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2820
3030
|
line: reference === void 0 ? line : nodeLine(reference)
|
|
2821
3031
|
});
|
|
2822
3032
|
};
|
|
2823
|
-
const checkContainer = (node) => {
|
|
3033
|
+
const checkContainer = (node, at) => {
|
|
2824
3034
|
node = resolveNode(node);
|
|
2825
3035
|
if (node === void 0) return;
|
|
2826
3036
|
const image = (0, yaml.isMap)(node) ? getNode(node, "image") : node;
|
|
2827
3037
|
checkRuntimeImage({
|
|
2828
3038
|
image: (0, yaml.isScalar)(image) ? image.value : void 0,
|
|
2829
3039
|
line: nodeLine(image ?? node),
|
|
2830
|
-
label: "container image"
|
|
3040
|
+
label: "container image",
|
|
3041
|
+
at
|
|
2831
3042
|
});
|
|
2832
3043
|
};
|
|
2833
|
-
const checkSteps = (node, floor) => {
|
|
3044
|
+
const checkSteps = (node, locationPrefix, floor) => {
|
|
2834
3045
|
currentEngineFloor = void 0;
|
|
2835
3046
|
checkoutBindings = [];
|
|
2836
3047
|
checkoutDestinations = /* @__PURE__ */ new Map();
|
|
2837
3048
|
unknownCheckoutDestination = false;
|
|
3049
|
+
unreviewableCheckoutSource = false;
|
|
2838
3050
|
node = resolveNode(node);
|
|
2839
3051
|
if (!(0, yaml.isSeq)(node)) return;
|
|
2840
3052
|
for (const step of node.items) {
|
|
@@ -2850,9 +3062,14 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2850
3062
|
}
|
|
2851
3063
|
currentEngineFloor = floor;
|
|
2852
3064
|
try {
|
|
2853
|
-
for (const step of node.items)
|
|
3065
|
+
for (const step of node.items) {
|
|
3066
|
+
const id = getNode(step, "id");
|
|
3067
|
+
currentLocation = (0, yaml.isScalar)(id) && typeof id.value === "string" ? `${locationPrefix}.steps.${id.value}` : void 0;
|
|
3068
|
+
checkAction(step);
|
|
3069
|
+
}
|
|
2854
3070
|
} finally {
|
|
2855
3071
|
currentEngineFloor = void 0;
|
|
3072
|
+
currentLocation = void 0;
|
|
2856
3073
|
}
|
|
2857
3074
|
};
|
|
2858
3075
|
try {
|
|
@@ -2866,15 +3083,15 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2866
3083
|
currentEngineFloor = void 0;
|
|
2867
3084
|
const job = getNode(jobs, key);
|
|
2868
3085
|
checkAction(job);
|
|
2869
|
-
checkContainer(getNode(job, "container"));
|
|
3086
|
+
checkContainer(getNode(job, "container"), `jobs.${key}.container`);
|
|
2870
3087
|
const services = getNode(job, "services");
|
|
2871
|
-
if ((0, yaml.isMap)(services)) for (const service of keysOf(services)) checkContainer(getNode(services, service));
|
|
2872
|
-
checkSteps(getNode(job, "steps"), engineFloors.find((entry) => entry.job === key));
|
|
3088
|
+
if ((0, yaml.isMap)(services)) for (const service of keysOf(services)) checkContainer(getNode(services, service), `jobs.${key}.services.${service}`);
|
|
3089
|
+
checkSteps(getNode(job, "steps"), `jobs.${key}`, engineFloors.find((entry) => entry.job === key));
|
|
2873
3090
|
}
|
|
2874
3091
|
} else {
|
|
2875
3092
|
const runs = getNode(document.contents, "runs");
|
|
2876
3093
|
const using = getNode(runs, "using");
|
|
2877
|
-
if ((0, yaml.isScalar)(using) && using.value === "composite") checkSteps(getNode(runs, "steps"));
|
|
3094
|
+
if ((0, yaml.isScalar)(using) && using.value === "composite") checkSteps(getNode(runs, "steps"), "runs");
|
|
2878
3095
|
if ((0, yaml.isScalar)(using) && using.value === "docker") {
|
|
2879
3096
|
const image = getNode(runs, "image");
|
|
2880
3097
|
const value = (0, yaml.isScalar)(image) ? image.value : void 0;
|
|
@@ -3103,7 +3320,7 @@ const record = (value) => typeof value === "object" && value !== null && !Array.
|
|
|
3103
3320
|
const array = (value) => Array.isArray(value);
|
|
3104
3321
|
/** Repository-owned declarations are explicit; malformed decisions fail closed. */
|
|
3105
3322
|
const parseToolchainConfiguration = (input) => {
|
|
3106
|
-
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
|
|
3323
|
+
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");
|
|
3107
3324
|
const disabled = /* @__PURE__ */ new Set();
|
|
3108
3325
|
const optOuts = input["optOuts"] ?? [];
|
|
3109
3326
|
if (!array(optOuts)) throw new Error("optOuts must be an array");
|
|
@@ -3116,7 +3333,8 @@ const parseToolchainConfiguration = (input) => {
|
|
|
3116
3333
|
return {
|
|
3117
3334
|
disabled,
|
|
3118
3335
|
cloud: parseCloudSetup(input["cloud"]),
|
|
3119
|
-
engineFloors: parseEngineFloors(input["engineFloors"])
|
|
3336
|
+
engineFloors: parseEngineFloors(input["engineFloors"]),
|
|
3337
|
+
dynamicSelectors: parseDynamicSelectors(input["dynamicSelectors"])
|
|
3120
3338
|
};
|
|
3121
3339
|
};
|
|
3122
3340
|
/** Read one tracked configuration snapshot for guards and policy generation. */
|
|
@@ -3182,10 +3400,12 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3182
3400
|
let disabled = /* @__PURE__ */ new Set();
|
|
3183
3401
|
let cloud;
|
|
3184
3402
|
const engineFloors = [];
|
|
3403
|
+
let dynamicSelectors = [];
|
|
3185
3404
|
if (files["stll-toolchain.json"] !== void 0) try {
|
|
3186
3405
|
const configuration = parseToolchainConfiguration(JSON.parse(files["stll-toolchain.json"]));
|
|
3187
3406
|
disabled = configuration.disabled;
|
|
3188
3407
|
cloud = configuration.cloud;
|
|
3408
|
+
dynamicSelectors = configuration.dynamicSelectors;
|
|
3189
3409
|
for (const entry of configuration.engineFloors) engineFloors.push(resolveEngineFloor(entry, files));
|
|
3190
3410
|
} catch (error) {
|
|
3191
3411
|
diagnostics.push({
|
|
@@ -3225,6 +3445,7 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3225
3445
|
}
|
|
3226
3446
|
const exercisedFloors = /* @__PURE__ */ new Set();
|
|
3227
3447
|
const mismatchedFloors = /* @__PURE__ */ new Set();
|
|
3448
|
+
const selectorMatches = /* @__PURE__ */ new Map();
|
|
3228
3449
|
diagnostics.push(...checkPackageFiles({
|
|
3229
3450
|
files,
|
|
3230
3451
|
policy
|
|
@@ -3240,6 +3461,8 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3240
3461
|
}
|
|
3241
3462
|
},
|
|
3242
3463
|
trackedFiles,
|
|
3464
|
+
dynamicSelectors: dynamicSelectors.filter((entry) => entry.path === file),
|
|
3465
|
+
onDynamicSelector: (entry) => selectorMatches.set(entry, (selectorMatches.get(entry) ?? 0) + 1),
|
|
3243
3466
|
repository,
|
|
3244
3467
|
onDelegated,
|
|
3245
3468
|
engineFloors: engineFloors.filter((entry) => entry.workflow === file),
|
|
@@ -3255,6 +3478,12 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3255
3478
|
line: 1,
|
|
3256
3479
|
message: `engine floor ${entry.workflow}:${entry.job} requires a setup-node exact patch satisfying ${entry.package} engines.node minimum major ${entry.major}`
|
|
3257
3480
|
});
|
|
3481
|
+
for (const entry of dynamicSelectors) if (selectorMatches.get(entry) !== 1) diagnostics.push({
|
|
3482
|
+
rule: "configuration",
|
|
3483
|
+
path: "stll-toolchain.json",
|
|
3484
|
+
line: 1,
|
|
3485
|
+
message: `dynamic selector ${entry.path}:${entry.at ?? entry.line} must match exactly one unresolved ${entry.kind} selector`
|
|
3486
|
+
});
|
|
3258
3487
|
diagnostics.push(...checkDependabot({
|
|
3259
3488
|
files,
|
|
3260
3489
|
policy: policy.dependabot
|