@stll/oxlint-config 0.12.1 → 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 +70 -6
- package/dist/toolchain-guard.cjs +259 -73
- package/dist/toolchain-guard.cjs.map +1 -1
- package/dist/toolchain-guard.mjs +259 -73
- package/dist/toolchain-guard.mjs.map +1 -1
- package/package.json +1 -1
package/dist/toolchain-guard.mjs
CHANGED
|
@@ -645,7 +645,7 @@ const generateCloudSetup = ({ policy, cloud, nodeVersion }) => {
|
|
|
645
645
|
};
|
|
646
646
|
//#endregion
|
|
647
647
|
//#region src/toolchain-container-inputs.ts
|
|
648
|
-
const record$
|
|
648
|
+
const record$7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
649
649
|
/** Filename class recognized by Dependabot's Docker Compose fetcher. */
|
|
650
650
|
const isComposeDefinitionPath = (file) => /^\.?(?:docker-)?compose(?:-[\w]+)?(?:\.[\w-]+)?\.ya?ml$/i.test(file.split("/").at(-1) ?? "");
|
|
651
651
|
/** Candidate YAML files are classified by their document shape after reading. */
|
|
@@ -692,11 +692,11 @@ const kubernetesPodSpecPaths = {
|
|
|
692
692
|
};
|
|
693
693
|
/** Shared semantic image enumeration for runtime checks and update-root generation. */
|
|
694
694
|
const containerDocumentImages = (document, file) => {
|
|
695
|
-
if (!record$
|
|
695
|
+
if (!record$7(document)) return void 0;
|
|
696
696
|
const services = document["services"];
|
|
697
|
-
if (isComposeDefinitionPath(file) && record$
|
|
697
|
+
if (isComposeDefinitionPath(file) && record$7(services)) {
|
|
698
698
|
const images = [];
|
|
699
|
-
for (const [name, service] of Object.entries(services)) if (record$
|
|
699
|
+
for (const [name, service] of Object.entries(services)) if (record$7(service) && service["image"] !== void 0) images.push({
|
|
700
700
|
image: service["image"],
|
|
701
701
|
path: [
|
|
702
702
|
"services",
|
|
@@ -717,7 +717,7 @@ const containerDocumentImages = (document, file) => {
|
|
|
717
717
|
visited.add(resource);
|
|
718
718
|
if (typeof resource["apiVersion"] !== "string" || typeof resource["kind"] !== "string") return;
|
|
719
719
|
if (resource["kind"] === "List" && Array.isArray(resource["items"])) {
|
|
720
|
-
for (const [index, item] of resource["items"].entries()) if (record$
|
|
720
|
+
for (const [index, item] of resource["items"].entries()) if (record$7(item)) walkResource(item, [
|
|
721
721
|
...prefix,
|
|
722
722
|
"items",
|
|
723
723
|
index
|
|
@@ -727,8 +727,8 @@ const containerDocumentImages = (document, file) => {
|
|
|
727
727
|
const specPath = Object.entries(kubernetesPodSpecPaths).find(([kind]) => kind === resource["kind"])?.[1];
|
|
728
728
|
if (specPath === void 0) return;
|
|
729
729
|
let spec = resource;
|
|
730
|
-
for (const key of specPath) spec = record$
|
|
731
|
-
if (!record$
|
|
730
|
+
for (const key of specPath) spec = record$7(spec) ? spec[key] : void 0;
|
|
731
|
+
if (!record$7(spec)) return;
|
|
732
732
|
for (const key of [
|
|
733
733
|
"containers",
|
|
734
734
|
"initContainers",
|
|
@@ -736,7 +736,7 @@ const containerDocumentImages = (document, file) => {
|
|
|
736
736
|
]) {
|
|
737
737
|
const containers = spec[key];
|
|
738
738
|
if (!Array.isArray(containers)) continue;
|
|
739
|
-
for (const [index, container] of containers.entries()) if (record$
|
|
739
|
+
for (const [index, container] of containers.entries()) if (record$7(container) && container["image"] !== void 0) images.push({
|
|
740
740
|
image: container["image"],
|
|
741
741
|
path: [
|
|
742
742
|
...prefix,
|
|
@@ -839,7 +839,7 @@ const workspaceContains = ({ directory, file, patterns, rootMembership }) => {
|
|
|
839
839
|
//#endregion
|
|
840
840
|
//#region src/toolchain-dependabot.ts
|
|
841
841
|
const dependabotRules = ["dependabot-policy"];
|
|
842
|
-
const record$
|
|
842
|
+
const record$6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
843
843
|
const strings = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
844
844
|
const sameStrings = (value, expected) => {
|
|
845
845
|
if (!strings(value) || value.length !== expected.length) return false;
|
|
@@ -861,8 +861,8 @@ const workspaceTable = (parsed, ecosystem) => {
|
|
|
861
861
|
case "cargo": return parsed["workspace"];
|
|
862
862
|
case "uv": {
|
|
863
863
|
const tool = parsed["tool"];
|
|
864
|
-
const uv = record$
|
|
865
|
-
return record$
|
|
864
|
+
const uv = record$6(tool) ? tool["uv"] : void 0;
|
|
865
|
+
return record$6(uv) ? uv["workspace"] : void 0;
|
|
866
866
|
}
|
|
867
867
|
default: throw new Error("unknown workspace ecosystem", { cause: ecosystem });
|
|
868
868
|
}
|
|
@@ -882,7 +882,7 @@ const workspaceRoots = (files, ecosystem) => {
|
|
|
882
882
|
const document = parseDocument(text);
|
|
883
883
|
if (document.errors.length > 0) throw new Error(`${workspaceFile}:1: invalid pnpm workspace YAML`);
|
|
884
884
|
const parsed = document.toJS({ maxAliasCount: 100 });
|
|
885
|
-
if (!record$
|
|
885
|
+
if (!record$6(parsed)) throw new Error(`${workspaceFile}:1: pnpm workspace must contain an object`);
|
|
886
886
|
const patterns = parsed["packages"] ?? [];
|
|
887
887
|
if (!strings(patterns)) throw new Error(`${workspaceFile}:1: pnpm packages must be a string array`);
|
|
888
888
|
workspaces.set(directory, patterns.map((pattern) => pattern.startsWith("!") ? `!${path.posix.normalize(pattern.slice(1))}` : path.posix.normalize(pattern)));
|
|
@@ -894,13 +894,13 @@ const workspaceRoots = (files, ecosystem) => {
|
|
|
894
894
|
}
|
|
895
895
|
try {
|
|
896
896
|
const parsed = ecosystem === "npm" ? JSON.parse(files[file] ?? "") : parse(files[file] ?? "");
|
|
897
|
-
if (!record$
|
|
897
|
+
if (!record$6(parsed)) continue;
|
|
898
898
|
if (ecosystem === "npm" && typeof parsed["packageManager"] === "string" && parsed["packageManager"].startsWith("pnpm@")) continue;
|
|
899
899
|
const value = workspaceTable(parsed, ecosystem);
|
|
900
|
-
const patterns = record$
|
|
901
|
-
const members = ecosystem === "cargo" && record$
|
|
900
|
+
const patterns = record$6(value) ? value[ecosystem === "npm" ? "packages" : "members"] : value;
|
|
901
|
+
const members = ecosystem === "cargo" && record$6(value) && patterns === void 0 ? [] : patterns;
|
|
902
902
|
if (strings(members)) {
|
|
903
|
-
const excluded = ecosystem !== "npm" && record$
|
|
903
|
+
const excluded = ecosystem !== "npm" && record$6(value) && strings(value["exclude"]) ? value["exclude"].map((pattern) => `!${pattern}`) : [];
|
|
904
904
|
workspaces.set(path.posix.dirname(file), [...members, ...excluded]);
|
|
905
905
|
}
|
|
906
906
|
} catch {}
|
|
@@ -951,11 +951,11 @@ const workspaceRoots = (files, ecosystem) => {
|
|
|
951
951
|
for (const directory of memberDirectories) try {
|
|
952
952
|
const parsed = parse(files[path.posix.join(directory, "Cargo.toml")] ?? "");
|
|
953
953
|
const workspace = parse(files[path.posix.join(root, "Cargo.toml")] ?? "")["workspace"];
|
|
954
|
-
const inherited = record$
|
|
954
|
+
const inherited = record$6(workspace) ? workspace["dependencies"] : void 0;
|
|
955
955
|
const dependencyOwners = [parsed];
|
|
956
956
|
const targets = parsed["target"];
|
|
957
|
-
if (record$
|
|
958
|
-
for (const target of Object.values(targets)) if (record$
|
|
957
|
+
if (record$6(targets)) {
|
|
958
|
+
for (const target of Object.values(targets)) if (record$6(target)) dependencyOwners.push(target);
|
|
959
959
|
}
|
|
960
960
|
for (const owner of dependencyOwners) for (const key of [
|
|
961
961
|
"dependencies",
|
|
@@ -963,12 +963,12 @@ const workspaceRoots = (files, ecosystem) => {
|
|
|
963
963
|
"build-dependencies"
|
|
964
964
|
]) {
|
|
965
965
|
const dependencies = owner[key];
|
|
966
|
-
if (!record$
|
|
966
|
+
if (!record$6(dependencies)) continue;
|
|
967
967
|
for (const [name, dependency] of Object.entries(dependencies)) {
|
|
968
|
-
if (!record$
|
|
968
|
+
if (!record$6(dependency)) continue;
|
|
969
969
|
const isInherited = dependency["workspace"] === true;
|
|
970
|
-
const resolved = isInherited && record$
|
|
971
|
-
if (!record$
|
|
970
|
+
const resolved = isInherited && record$6(inherited) ? inherited[name] : dependency;
|
|
971
|
+
if (!record$6(resolved) || typeof resolved["path"] !== "string") continue;
|
|
972
972
|
if (path.posix.isAbsolute(resolved["path"]) || /\\|\$|^[A-Za-z]:/.test(resolved["path"])) continue;
|
|
973
973
|
const target = path.posix.normalize(path.posix.join(isInherited ? root : directory, resolved["path"]));
|
|
974
974
|
const relative = path.posix.relative(root, target);
|
|
@@ -996,7 +996,7 @@ const javascriptEcosystem = (directory, files) => {
|
|
|
996
996
|
if (files[path.posix.join(root, "bun.lock")] !== void 0) return "bun";
|
|
997
997
|
try {
|
|
998
998
|
const manifest = JSON.parse(files[path.posix.join(root, "package.json")] ?? "{}");
|
|
999
|
-
if (record$
|
|
999
|
+
if (record$6(manifest) && typeof manifest["packageManager"] === "string" && manifest["packageManager"].startsWith("bun@")) return "bun";
|
|
1000
1000
|
} catch {}
|
|
1001
1001
|
return "npm";
|
|
1002
1002
|
};
|
|
@@ -1008,7 +1008,7 @@ const pythonEcosystem = (file, files) => {
|
|
|
1008
1008
|
const pyproject = files[path.posix.join(root, "pyproject.toml")];
|
|
1009
1009
|
if (pyproject !== void 0) try {
|
|
1010
1010
|
const tool = parse(pyproject)["tool"];
|
|
1011
|
-
if (record$
|
|
1011
|
+
if (record$6(tool) && record$6(tool["uv"])) return "uv";
|
|
1012
1012
|
} catch {}
|
|
1013
1013
|
return "pip";
|
|
1014
1014
|
};
|
|
@@ -1079,7 +1079,7 @@ const checkDependabot = ({ files, policy }) => {
|
|
|
1079
1079
|
add("cannot resolve Dependabot YAML aliases");
|
|
1080
1080
|
return diagnostics;
|
|
1081
1081
|
}
|
|
1082
|
-
if (!record$
|
|
1082
|
+
if (!record$6(parsed) || parsed["version"] !== 2 || !Array.isArray(parsed["updates"])) {
|
|
1083
1083
|
add("Dependabot must declare version: 2 and an updates array");
|
|
1084
1084
|
return diagnostics;
|
|
1085
1085
|
}
|
|
@@ -1092,7 +1092,7 @@ const checkDependabot = ({ files, policy }) => {
|
|
|
1092
1092
|
});
|
|
1093
1093
|
if (legacyDocument.errors.length === 0) legacy = legacyDocument.toJS({ maxAliasCount: 100 });
|
|
1094
1094
|
} catch {}
|
|
1095
|
-
const legacyUpdates = record$
|
|
1095
|
+
const legacyUpdates = record$6(legacy) && Array.isArray(legacy["updates"]) ? legacy["updates"] : [];
|
|
1096
1096
|
const rootClaims = /* @__PURE__ */ new Map();
|
|
1097
1097
|
const directoryClaims = /* @__PURE__ */ new Map();
|
|
1098
1098
|
for (const [index, update] of parsed["updates"].entries()) {
|
|
@@ -1106,7 +1106,7 @@ const checkDependabot = ({ files, policy }) => {
|
|
|
1106
1106
|
return offset === void 0 ? 1 : lineCounter.linePos(offset).line;
|
|
1107
1107
|
};
|
|
1108
1108
|
const report = (key, message) => add(message, lineOf(key));
|
|
1109
|
-
if (!record$
|
|
1109
|
+
if (!record$6(update) || typeof update["package-ecosystem"] !== "string") {
|
|
1110
1110
|
report("package-ecosystem", "each Dependabot update must declare a package ecosystem");
|
|
1111
1111
|
continue;
|
|
1112
1112
|
}
|
|
@@ -1140,26 +1140,75 @@ const checkDependabot = ({ files, policy }) => {
|
|
|
1140
1140
|
covered.set(ecosystem, matches);
|
|
1141
1141
|
const schedule = update["schedule"];
|
|
1142
1142
|
const legacyUpdate = legacyUpdates.at(index);
|
|
1143
|
-
const legacySchedule = record$
|
|
1144
|
-
if (record$
|
|
1145
|
-
if (!record$
|
|
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`);
|
|
1146
1146
|
const cooldown = update["cooldown"];
|
|
1147
|
-
if (!record$
|
|
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}`);
|
|
1148
1148
|
const groups = update["groups"];
|
|
1149
|
-
if (!record$
|
|
1149
|
+
if (!record$6(groups) || Object.keys(groups).length !== Object.keys(policy.groups).length || Object.entries(policy.groups).some(([name, expected]) => {
|
|
1150
1150
|
const group = groups[name];
|
|
1151
|
-
return !record$
|
|
1151
|
+
return !record$6(group) || Object.keys(group).length !== 2 || !sameStrings(group["patterns"], expected.patterns) || !sameStrings(group["update-types"], expected.updateTypes);
|
|
1152
1152
|
})) report("groups", `${ecosystem}: groups must match the shared policy`);
|
|
1153
1153
|
const ignored = ignoredFor(ecosystem, policy);
|
|
1154
1154
|
const ignore = update["ignore"];
|
|
1155
1155
|
if (ignore !== void 0 && (!Array.isArray(ignore) || ignore.length === 0)) report("ignore", `${ecosystem}: omit ignore or provide a nonempty array`);
|
|
1156
|
-
if (Array.isArray(ignore) && ignore.some((entry) => !record$
|
|
1157
|
-
for (const name of ignored) if (!Array.isArray(ignore) || !ignore.some((entry) => record$
|
|
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}`);
|
|
1158
1158
|
}
|
|
1159
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}`);
|
|
1160
1160
|
return diagnostics;
|
|
1161
1161
|
};
|
|
1162
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
|
|
1163
1212
|
//#region src/toolchain-engine-floors.ts
|
|
1164
1213
|
const record$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1165
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 !== "..");
|
|
@@ -1971,7 +2020,7 @@ const pythonRequirementMatches = (requirement, version) => {
|
|
|
1971
2020
|
});
|
|
1972
2021
|
};
|
|
1973
2022
|
/** Check runtime declarations only in the files that declare or configure them. */
|
|
1974
|
-
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 }) => {
|
|
1975
2024
|
const diagnostics = [];
|
|
1976
2025
|
const name = path.posix.basename(file);
|
|
1977
2026
|
const lines = text.split(/\r?\n/);
|
|
@@ -1983,6 +2032,16 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
1983
2032
|
message
|
|
1984
2033
|
});
|
|
1985
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
|
+
};
|
|
1986
2045
|
const lineOf = (key) => Math.max(1, lines.findIndex((line) => line.includes(key)) + 1);
|
|
1987
2046
|
const pin = ({ rule, value, expected, label, line = 1, selector }) => {
|
|
1988
2047
|
let matches = value === expected;
|
|
@@ -2184,12 +2243,13 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2184
2243
|
line: index + 1
|
|
2185
2244
|
});
|
|
2186
2245
|
});
|
|
2187
|
-
const checkRuntimeImage = ({ image, line, label }) => {
|
|
2246
|
+
const checkRuntimeImage = ({ image, line, label, at }) => {
|
|
2188
2247
|
const unknownRuntime = () => {
|
|
2248
|
+
if (dynamic(image) && authorizeDynamic("image", line, at)) return;
|
|
2189
2249
|
add({
|
|
2190
2250
|
rule: "runtime-docker",
|
|
2191
2251
|
line,
|
|
2192
|
-
message: `cannot determine ${label} runtime; use a static image or a
|
|
2252
|
+
message: `cannot determine ${label} runtime; use a static image or a scoped dynamicSelectors declaration`
|
|
2193
2253
|
});
|
|
2194
2254
|
};
|
|
2195
2255
|
if (typeof image !== "string") {
|
|
@@ -2418,6 +2478,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2418
2478
|
checkRuntimeImage({
|
|
2419
2479
|
image: entry.image,
|
|
2420
2480
|
line,
|
|
2481
|
+
at: entry.path.join("."),
|
|
2421
2482
|
label: classified?.ecosystem === "docker-compose" ? "Compose image" : "Kubernetes image"
|
|
2422
2483
|
});
|
|
2423
2484
|
}
|
|
@@ -2452,8 +2513,22 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2452
2513
|
let checkoutBindings = [];
|
|
2453
2514
|
let checkoutDestinations = /* @__PURE__ */ new Map();
|
|
2454
2515
|
let unknownCheckoutDestination = false;
|
|
2516
|
+
let unreviewableCheckoutSource = false;
|
|
2455
2517
|
const staticRepositoryPath = (value) => typeof value === "string" && value !== "" && !value.includes("${{") && !value.includes("\\") && !value.includes(":") && !path.posix.isAbsolute(value) && !value.split("/").includes("..");
|
|
2456
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
|
+
};
|
|
2457
2532
|
const checkReference = ({ value, tool, line }) => {
|
|
2458
2533
|
const allowed = {
|
|
2459
2534
|
node: [".node-version", ".nvmrc"],
|
|
@@ -2462,8 +2537,28 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2462
2537
|
}[tool];
|
|
2463
2538
|
const rule = tool === "bun" ? "bun-pins" : "runtime-workflow";
|
|
2464
2539
|
const selector = staticRepositoryPath(value) ? path.posix.normalize(value) : void 0;
|
|
2465
|
-
const
|
|
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
|
+
}
|
|
2466
2560
|
if (selector === void 0) {
|
|
2561
|
+
if (tool === "bun" && dynamic(value) && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
|
|
2467
2562
|
add({
|
|
2468
2563
|
rule,
|
|
2469
2564
|
line,
|
|
@@ -2471,7 +2566,8 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2471
2566
|
});
|
|
2472
2567
|
return;
|
|
2473
2568
|
}
|
|
2474
|
-
if (unknownCheckoutDestination ||
|
|
2569
|
+
if (unknownCheckoutDestination || hasAmbiguousCheckoutWriters(selector, providers)) {
|
|
2570
|
+
if (tool === "bun" && dynamicSource && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
|
|
2475
2571
|
add({
|
|
2476
2572
|
rule,
|
|
2477
2573
|
line,
|
|
@@ -2480,6 +2576,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2480
2576
|
return;
|
|
2481
2577
|
}
|
|
2482
2578
|
if (binding?.source === "untrusted") {
|
|
2579
|
+
if (tool === "bun" && dynamicSource && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
|
|
2483
2580
|
add({
|
|
2484
2581
|
rule,
|
|
2485
2582
|
line: binding.line,
|
|
@@ -2496,13 +2593,15 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2496
2593
|
});
|
|
2497
2594
|
return;
|
|
2498
2595
|
}
|
|
2499
|
-
if (binding !== void 0 && (binding.sparse.mode === "
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
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
|
+
}
|
|
2506
2605
|
}
|
|
2507
2606
|
if (binding?.source === "delegated") {
|
|
2508
2607
|
onDelegated?.({
|
|
@@ -2624,10 +2723,10 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2624
2723
|
return inputs;
|
|
2625
2724
|
};
|
|
2626
2725
|
const getInput = (node, key) => inputsOf(node).get(key.toUpperCase());
|
|
2627
|
-
const
|
|
2628
|
-
if (
|
|
2726
|
+
const expressionAt = (value, start) => {
|
|
2727
|
+
if (!value.startsWith("${{", start)) return void 0;
|
|
2629
2728
|
let quote;
|
|
2630
|
-
for (let index = 3; index < value.length; index++) {
|
|
2729
|
+
for (let index = start + 3; index < value.length; index++) {
|
|
2631
2730
|
const character = value.at(index);
|
|
2632
2731
|
if (quote !== void 0) {
|
|
2633
2732
|
if (character === quote) if (quote === "'" && value.at(index + 1) === "'") index++;
|
|
@@ -2637,9 +2736,33 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2637
2736
|
}
|
|
2638
2737
|
if (character === "'" || character === "\"") quote = character;
|
|
2639
2738
|
else if (value.startsWith("${{", index)) return void 0;
|
|
2640
|
-
else if (character === "}" && value.at(index + 1) === "}") return
|
|
2739
|
+
else if (character === "}" && value.at(index + 1) === "}") return {
|
|
2740
|
+
body: value.slice(start + 3, index).trim(),
|
|
2741
|
+
end: index + 2
|
|
2742
|
+
};
|
|
2641
2743
|
}
|
|
2642
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
|
+
};
|
|
2643
2766
|
const checkoutPrefixOf = (node) => {
|
|
2644
2767
|
const destination = getInput(node, "path");
|
|
2645
2768
|
if (destination === void 0) return ".";
|
|
@@ -2652,11 +2775,19 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2652
2775
|
const value = getInput(node, "sparse-checkout");
|
|
2653
2776
|
const cone = getInput(node, "sparse-checkout-cone-mode");
|
|
2654
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" };
|
|
2655
|
-
const paths = value.value.split(/\r?\n/).map((entry) => entry.trim()).filter((entry) => entry !== "");
|
|
2656
|
-
if (paths.length === 0 || paths.some((entry) => !
|
|
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
|
+
}
|
|
2657
2788
|
return {
|
|
2658
2789
|
mode: "files",
|
|
2659
|
-
paths:
|
|
2790
|
+
paths: literalPaths
|
|
2660
2791
|
};
|
|
2661
2792
|
};
|
|
2662
2793
|
let currentEngineFloor;
|
|
@@ -2686,8 +2817,9 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2686
2817
|
}
|
|
2687
2818
|
if (value.startsWith("./")) {
|
|
2688
2819
|
const selector = staticRepositoryPath(value) ? path.posix.normalize(value) : void 0;
|
|
2689
|
-
const
|
|
2690
|
-
|
|
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({
|
|
2691
2823
|
rule: "action-pins",
|
|
2692
2824
|
line,
|
|
2693
2825
|
message: "local actions require an unambiguous checkout of the current source snapshot"
|
|
@@ -2743,24 +2875,55 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2743
2875
|
const checkoutRepository = getInput(node, "repository");
|
|
2744
2876
|
const checkoutRef = getInput(node, "ref");
|
|
2745
2877
|
const prefix = checkoutPrefixOf(node);
|
|
2878
|
+
const clean = getInput(node, "clean");
|
|
2879
|
+
const cleanIsResolved = clean === void 0 || isScalar(clean) && !dynamic(clean.value);
|
|
2746
2880
|
let repo;
|
|
2747
2881
|
if (checkoutRepository !== void 0) repo = isScalar(checkoutRepository) ? checkoutRepository.value : null;
|
|
2748
2882
|
const self = repo === void 0 || repo === "${{ github.repository }}" || typeof repo === "string" && repository !== void 0 && repo.toLowerCase() === repository.toLowerCase();
|
|
2749
2883
|
const workflowSource = repo === "${{ job.workflow_repository }}" && isScalar(checkoutRef) && checkoutRef.value === "${{ job.workflow_sha }}";
|
|
2750
|
-
const
|
|
2751
|
-
const
|
|
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");
|
|
2752
2895
|
const dynamicRef = dynamicRefBody(refValue);
|
|
2753
|
-
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;
|
|
2754
2905
|
if (!staticRepositoryPath(prefix)) checkoutBindings.push({
|
|
2755
2906
|
path: ".",
|
|
2907
|
+
dynamicSource: validSource && dynamic(prefix),
|
|
2756
2908
|
source: "untrusted",
|
|
2757
2909
|
reason: "checkout destination must be a static repository path",
|
|
2758
2910
|
line,
|
|
2759
|
-
sparse:
|
|
2911
|
+
sparse: sparseCheckoutOf(node)
|
|
2760
2912
|
});
|
|
2761
2913
|
else {
|
|
2762
2914
|
const destination = normalizeCheckoutPath(prefix);
|
|
2763
|
-
|
|
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({
|
|
2764
2927
|
node,
|
|
2765
2928
|
key: "if"
|
|
2766
2929
|
}) === void 0 && sourceMap({
|
|
@@ -2769,18 +2932,21 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2769
2932
|
}) === void 0 && checkoutDestinations.get(destination) === 1 && !unknownCheckoutDestination;
|
|
2770
2933
|
if (trustedWriter && delegatedSource && typeof refValue === "string") checkoutBindings.push({
|
|
2771
2934
|
path: destination,
|
|
2935
|
+
dynamicSource: validSource && delegatedSource,
|
|
2772
2936
|
sparse: sparseCheckoutOf(node),
|
|
2773
2937
|
source: "delegated",
|
|
2774
2938
|
ref: refValue
|
|
2775
2939
|
});
|
|
2776
2940
|
else if (trustedWriter && (workflowSource || currentSource)) checkoutBindings.push({
|
|
2777
2941
|
path: destination,
|
|
2942
|
+
dynamicSource: validSource && delegatedSource,
|
|
2778
2943
|
sparse: sparseCheckoutOf(node),
|
|
2779
2944
|
source: "tracked"
|
|
2780
2945
|
});
|
|
2781
2946
|
else {
|
|
2782
2947
|
let reason = "checkout does not select a trusted repository source snapshot";
|
|
2783
|
-
if (
|
|
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";
|
|
2784
2950
|
else if (sourceMap({
|
|
2785
2951
|
node,
|
|
2786
2952
|
key: "if"
|
|
@@ -2791,6 +2957,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2791
2957
|
else if (checkoutDestinations.get(destination) !== 1 || unknownCheckoutDestination) reason = "checkout destination must have exactly one static writer";
|
|
2792
2958
|
checkoutBindings.push({
|
|
2793
2959
|
path: destination,
|
|
2960
|
+
dynamicSource: validSource && delegatedSource,
|
|
2794
2961
|
sparse: sparseCheckoutOf(node),
|
|
2795
2962
|
source: "untrusted",
|
|
2796
2963
|
reason,
|
|
@@ -2839,21 +3006,23 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2839
3006
|
line: reference === void 0 ? line : nodeLine(reference)
|
|
2840
3007
|
});
|
|
2841
3008
|
};
|
|
2842
|
-
const checkContainer = (node) => {
|
|
3009
|
+
const checkContainer = (node, at) => {
|
|
2843
3010
|
node = resolveNode(node);
|
|
2844
3011
|
if (node === void 0) return;
|
|
2845
3012
|
const image = isMap(node) ? getNode(node, "image") : node;
|
|
2846
3013
|
checkRuntimeImage({
|
|
2847
3014
|
image: isScalar(image) ? image.value : void 0,
|
|
2848
3015
|
line: nodeLine(image ?? node),
|
|
2849
|
-
label: "container image"
|
|
3016
|
+
label: "container image",
|
|
3017
|
+
at
|
|
2850
3018
|
});
|
|
2851
3019
|
};
|
|
2852
|
-
const checkSteps = (node, floor) => {
|
|
3020
|
+
const checkSteps = (node, locationPrefix, floor) => {
|
|
2853
3021
|
currentEngineFloor = void 0;
|
|
2854
3022
|
checkoutBindings = [];
|
|
2855
3023
|
checkoutDestinations = /* @__PURE__ */ new Map();
|
|
2856
3024
|
unknownCheckoutDestination = false;
|
|
3025
|
+
unreviewableCheckoutSource = false;
|
|
2857
3026
|
node = resolveNode(node);
|
|
2858
3027
|
if (!isSeq(node)) return;
|
|
2859
3028
|
for (const step of node.items) {
|
|
@@ -2869,9 +3038,14 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2869
3038
|
}
|
|
2870
3039
|
currentEngineFloor = floor;
|
|
2871
3040
|
try {
|
|
2872
|
-
for (const step of node.items)
|
|
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
|
+
}
|
|
2873
3046
|
} finally {
|
|
2874
3047
|
currentEngineFloor = void 0;
|
|
3048
|
+
currentLocation = void 0;
|
|
2875
3049
|
}
|
|
2876
3050
|
};
|
|
2877
3051
|
try {
|
|
@@ -2885,15 +3059,15 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2885
3059
|
currentEngineFloor = void 0;
|
|
2886
3060
|
const job = getNode(jobs, key);
|
|
2887
3061
|
checkAction(job);
|
|
2888
|
-
checkContainer(getNode(job, "container"));
|
|
3062
|
+
checkContainer(getNode(job, "container"), `jobs.${key}.container`);
|
|
2889
3063
|
const services = getNode(job, "services");
|
|
2890
|
-
if (isMap(services)) for (const service of keysOf(services)) checkContainer(getNode(services, service));
|
|
2891
|
-
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));
|
|
2892
3066
|
}
|
|
2893
3067
|
} else {
|
|
2894
3068
|
const runs = getNode(document.contents, "runs");
|
|
2895
3069
|
const using = getNode(runs, "using");
|
|
2896
|
-
if (isScalar(using) && using.value === "composite") checkSteps(getNode(runs, "steps"));
|
|
3070
|
+
if (isScalar(using) && using.value === "composite") checkSteps(getNode(runs, "steps"), "runs");
|
|
2897
3071
|
if (isScalar(using) && using.value === "docker") {
|
|
2898
3072
|
const image = getNode(runs, "image");
|
|
2899
3073
|
const value = isScalar(image) ? image.value : void 0;
|
|
@@ -3122,7 +3296,7 @@ const record = (value) => typeof value === "object" && value !== null && !Array.
|
|
|
3122
3296
|
const array = (value) => Array.isArray(value);
|
|
3123
3297
|
/** Repository-owned declarations are explicit; malformed decisions fail closed. */
|
|
3124
3298
|
const parseToolchainConfiguration = (input) => {
|
|
3125
|
-
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
|
|
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");
|
|
3126
3300
|
const disabled = /* @__PURE__ */ new Set();
|
|
3127
3301
|
const optOuts = input["optOuts"] ?? [];
|
|
3128
3302
|
if (!array(optOuts)) throw new Error("optOuts must be an array");
|
|
@@ -3135,7 +3309,8 @@ const parseToolchainConfiguration = (input) => {
|
|
|
3135
3309
|
return {
|
|
3136
3310
|
disabled,
|
|
3137
3311
|
cloud: parseCloudSetup(input["cloud"]),
|
|
3138
|
-
engineFloors: parseEngineFloors(input["engineFloors"])
|
|
3312
|
+
engineFloors: parseEngineFloors(input["engineFloors"]),
|
|
3313
|
+
dynamicSelectors: parseDynamicSelectors(input["dynamicSelectors"])
|
|
3139
3314
|
};
|
|
3140
3315
|
};
|
|
3141
3316
|
/** Read one tracked configuration snapshot for guards and policy generation. */
|
|
@@ -3201,10 +3376,12 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3201
3376
|
let disabled = /* @__PURE__ */ new Set();
|
|
3202
3377
|
let cloud;
|
|
3203
3378
|
const engineFloors = [];
|
|
3379
|
+
let dynamicSelectors = [];
|
|
3204
3380
|
if (files["stll-toolchain.json"] !== void 0) try {
|
|
3205
3381
|
const configuration = parseToolchainConfiguration(JSON.parse(files["stll-toolchain.json"]));
|
|
3206
3382
|
disabled = configuration.disabled;
|
|
3207
3383
|
cloud = configuration.cloud;
|
|
3384
|
+
dynamicSelectors = configuration.dynamicSelectors;
|
|
3208
3385
|
for (const entry of configuration.engineFloors) engineFloors.push(resolveEngineFloor(entry, files));
|
|
3209
3386
|
} catch (error) {
|
|
3210
3387
|
diagnostics.push({
|
|
@@ -3244,6 +3421,7 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3244
3421
|
}
|
|
3245
3422
|
const exercisedFloors = /* @__PURE__ */ new Set();
|
|
3246
3423
|
const mismatchedFloors = /* @__PURE__ */ new Set();
|
|
3424
|
+
const selectorMatches = /* @__PURE__ */ new Map();
|
|
3247
3425
|
diagnostics.push(...checkPackageFiles({
|
|
3248
3426
|
files,
|
|
3249
3427
|
policy
|
|
@@ -3259,6 +3437,8 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3259
3437
|
}
|
|
3260
3438
|
},
|
|
3261
3439
|
trackedFiles,
|
|
3440
|
+
dynamicSelectors: dynamicSelectors.filter((entry) => entry.path === file),
|
|
3441
|
+
onDynamicSelector: (entry) => selectorMatches.set(entry, (selectorMatches.get(entry) ?? 0) + 1),
|
|
3262
3442
|
repository,
|
|
3263
3443
|
onDelegated,
|
|
3264
3444
|
engineFloors: engineFloors.filter((entry) => entry.workflow === file),
|
|
@@ -3274,6 +3454,12 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3274
3454
|
line: 1,
|
|
3275
3455
|
message: `engine floor ${entry.workflow}:${entry.job} requires a setup-node exact patch satisfying ${entry.package} engines.node minimum major ${entry.major}`
|
|
3276
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
|
+
});
|
|
3277
3463
|
diagnostics.push(...checkDependabot({
|
|
3278
3464
|
files,
|
|
3279
3465
|
policy: policy.dependabot
|