@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.cjs
CHANGED
|
@@ -669,7 +669,7 @@ const generateCloudSetup = ({ policy, cloud, nodeVersion }) => {
|
|
|
669
669
|
};
|
|
670
670
|
//#endregion
|
|
671
671
|
//#region src/toolchain-container-inputs.ts
|
|
672
|
-
const record$
|
|
672
|
+
const record$7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
673
673
|
/** Filename class recognized by Dependabot's Docker Compose fetcher. */
|
|
674
674
|
const isComposeDefinitionPath = (file) => /^\.?(?:docker-)?compose(?:-[\w]+)?(?:\.[\w-]+)?\.ya?ml$/i.test(file.split("/").at(-1) ?? "");
|
|
675
675
|
/** Candidate YAML files are classified by their document shape after reading. */
|
|
@@ -716,11 +716,11 @@ const kubernetesPodSpecPaths = {
|
|
|
716
716
|
};
|
|
717
717
|
/** Shared semantic image enumeration for runtime checks and update-root generation. */
|
|
718
718
|
const containerDocumentImages = (document, file) => {
|
|
719
|
-
if (!record$
|
|
719
|
+
if (!record$7(document)) return void 0;
|
|
720
720
|
const services = document["services"];
|
|
721
|
-
if (isComposeDefinitionPath(file) && record$
|
|
721
|
+
if (isComposeDefinitionPath(file) && record$7(services)) {
|
|
722
722
|
const images = [];
|
|
723
|
-
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({
|
|
724
724
|
image: service["image"],
|
|
725
725
|
path: [
|
|
726
726
|
"services",
|
|
@@ -741,7 +741,7 @@ const containerDocumentImages = (document, file) => {
|
|
|
741
741
|
visited.add(resource);
|
|
742
742
|
if (typeof resource["apiVersion"] !== "string" || typeof resource["kind"] !== "string") return;
|
|
743
743
|
if (resource["kind"] === "List" && Array.isArray(resource["items"])) {
|
|
744
|
-
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, [
|
|
745
745
|
...prefix,
|
|
746
746
|
"items",
|
|
747
747
|
index
|
|
@@ -751,8 +751,8 @@ const containerDocumentImages = (document, file) => {
|
|
|
751
751
|
const specPath = Object.entries(kubernetesPodSpecPaths).find(([kind]) => kind === resource["kind"])?.[1];
|
|
752
752
|
if (specPath === void 0) return;
|
|
753
753
|
let spec = resource;
|
|
754
|
-
for (const key of specPath) spec = record$
|
|
755
|
-
if (!record$
|
|
754
|
+
for (const key of specPath) spec = record$7(spec) ? spec[key] : void 0;
|
|
755
|
+
if (!record$7(spec)) return;
|
|
756
756
|
for (const key of [
|
|
757
757
|
"containers",
|
|
758
758
|
"initContainers",
|
|
@@ -760,7 +760,7 @@ const containerDocumentImages = (document, file) => {
|
|
|
760
760
|
]) {
|
|
761
761
|
const containers = spec[key];
|
|
762
762
|
if (!Array.isArray(containers)) continue;
|
|
763
|
-
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({
|
|
764
764
|
image: container["image"],
|
|
765
765
|
path: [
|
|
766
766
|
...prefix,
|
|
@@ -863,7 +863,7 @@ const workspaceContains = ({ directory, file, patterns, rootMembership }) => {
|
|
|
863
863
|
//#endregion
|
|
864
864
|
//#region src/toolchain-dependabot.ts
|
|
865
865
|
const dependabotRules = ["dependabot-policy"];
|
|
866
|
-
const record$
|
|
866
|
+
const record$6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
867
867
|
const strings = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
868
868
|
const sameStrings = (value, expected) => {
|
|
869
869
|
if (!strings(value) || value.length !== expected.length) return false;
|
|
@@ -885,8 +885,8 @@ const workspaceTable = (parsed, ecosystem) => {
|
|
|
885
885
|
case "cargo": return parsed["workspace"];
|
|
886
886
|
case "uv": {
|
|
887
887
|
const tool = parsed["tool"];
|
|
888
|
-
const uv = record$
|
|
889
|
-
return record$
|
|
888
|
+
const uv = record$6(tool) ? tool["uv"] : void 0;
|
|
889
|
+
return record$6(uv) ? uv["workspace"] : void 0;
|
|
890
890
|
}
|
|
891
891
|
default: throw new Error("unknown workspace ecosystem", { cause: ecosystem });
|
|
892
892
|
}
|
|
@@ -906,7 +906,7 @@ const workspaceRoots = (files, ecosystem) => {
|
|
|
906
906
|
const document = (0, yaml.parseDocument)(text);
|
|
907
907
|
if (document.errors.length > 0) throw new Error(`${workspaceFile}:1: invalid pnpm workspace YAML`);
|
|
908
908
|
const parsed = document.toJS({ maxAliasCount: 100 });
|
|
909
|
-
if (!record$
|
|
909
|
+
if (!record$6(parsed)) throw new Error(`${workspaceFile}:1: pnpm workspace must contain an object`);
|
|
910
910
|
const patterns = parsed["packages"] ?? [];
|
|
911
911
|
if (!strings(patterns)) throw new Error(`${workspaceFile}:1: pnpm packages must be a string array`);
|
|
912
912
|
workspaces.set(directory, patterns.map((pattern) => pattern.startsWith("!") ? `!${node_path.default.posix.normalize(pattern.slice(1))}` : node_path.default.posix.normalize(pattern)));
|
|
@@ -918,13 +918,13 @@ const workspaceRoots = (files, ecosystem) => {
|
|
|
918
918
|
}
|
|
919
919
|
try {
|
|
920
920
|
const parsed = ecosystem === "npm" ? JSON.parse(files[file] ?? "") : (0, smol_toml.parse)(files[file] ?? "");
|
|
921
|
-
if (!record$
|
|
921
|
+
if (!record$6(parsed)) continue;
|
|
922
922
|
if (ecosystem === "npm" && typeof parsed["packageManager"] === "string" && parsed["packageManager"].startsWith("pnpm@")) continue;
|
|
923
923
|
const value = workspaceTable(parsed, ecosystem);
|
|
924
|
-
const patterns = record$
|
|
925
|
-
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;
|
|
926
926
|
if (strings(members)) {
|
|
927
|
-
const excluded = ecosystem !== "npm" && record$
|
|
927
|
+
const excluded = ecosystem !== "npm" && record$6(value) && strings(value["exclude"]) ? value["exclude"].map((pattern) => `!${pattern}`) : [];
|
|
928
928
|
workspaces.set(node_path.default.posix.dirname(file), [...members, ...excluded]);
|
|
929
929
|
}
|
|
930
930
|
} catch {}
|
|
@@ -975,11 +975,11 @@ const workspaceRoots = (files, ecosystem) => {
|
|
|
975
975
|
for (const directory of memberDirectories) try {
|
|
976
976
|
const parsed = (0, smol_toml.parse)(files[node_path.default.posix.join(directory, "Cargo.toml")] ?? "");
|
|
977
977
|
const workspace = (0, smol_toml.parse)(files[node_path.default.posix.join(root, "Cargo.toml")] ?? "")["workspace"];
|
|
978
|
-
const inherited = record$
|
|
978
|
+
const inherited = record$6(workspace) ? workspace["dependencies"] : void 0;
|
|
979
979
|
const dependencyOwners = [parsed];
|
|
980
980
|
const targets = parsed["target"];
|
|
981
|
-
if (record$
|
|
982
|
-
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);
|
|
983
983
|
}
|
|
984
984
|
for (const owner of dependencyOwners) for (const key of [
|
|
985
985
|
"dependencies",
|
|
@@ -987,12 +987,12 @@ const workspaceRoots = (files, ecosystem) => {
|
|
|
987
987
|
"build-dependencies"
|
|
988
988
|
]) {
|
|
989
989
|
const dependencies = owner[key];
|
|
990
|
-
if (!record$
|
|
990
|
+
if (!record$6(dependencies)) continue;
|
|
991
991
|
for (const [name, dependency] of Object.entries(dependencies)) {
|
|
992
|
-
if (!record$
|
|
992
|
+
if (!record$6(dependency)) continue;
|
|
993
993
|
const isInherited = dependency["workspace"] === true;
|
|
994
|
-
const resolved = isInherited && record$
|
|
995
|
-
if (!record$
|
|
994
|
+
const resolved = isInherited && record$6(inherited) ? inherited[name] : dependency;
|
|
995
|
+
if (!record$6(resolved) || typeof resolved["path"] !== "string") continue;
|
|
996
996
|
if (node_path.default.posix.isAbsolute(resolved["path"]) || /\\|\$|^[A-Za-z]:/.test(resolved["path"])) continue;
|
|
997
997
|
const target = node_path.default.posix.normalize(node_path.default.posix.join(isInherited ? root : directory, resolved["path"]));
|
|
998
998
|
const relative = node_path.default.posix.relative(root, target);
|
|
@@ -1020,7 +1020,7 @@ const javascriptEcosystem = (directory, files) => {
|
|
|
1020
1020
|
if (files[node_path.default.posix.join(root, "bun.lock")] !== void 0) return "bun";
|
|
1021
1021
|
try {
|
|
1022
1022
|
const manifest = JSON.parse(files[node_path.default.posix.join(root, "package.json")] ?? "{}");
|
|
1023
|
-
if (record$
|
|
1023
|
+
if (record$6(manifest) && typeof manifest["packageManager"] === "string" && manifest["packageManager"].startsWith("bun@")) return "bun";
|
|
1024
1024
|
} catch {}
|
|
1025
1025
|
return "npm";
|
|
1026
1026
|
};
|
|
@@ -1032,7 +1032,7 @@ const pythonEcosystem = (file, files) => {
|
|
|
1032
1032
|
const pyproject = files[node_path.default.posix.join(root, "pyproject.toml")];
|
|
1033
1033
|
if (pyproject !== void 0) try {
|
|
1034
1034
|
const tool = (0, smol_toml.parse)(pyproject)["tool"];
|
|
1035
|
-
if (record$
|
|
1035
|
+
if (record$6(tool) && record$6(tool["uv"])) return "uv";
|
|
1036
1036
|
} catch {}
|
|
1037
1037
|
return "pip";
|
|
1038
1038
|
};
|
|
@@ -1103,7 +1103,7 @@ const checkDependabot = ({ files, policy }) => {
|
|
|
1103
1103
|
add("cannot resolve Dependabot YAML aliases");
|
|
1104
1104
|
return diagnostics;
|
|
1105
1105
|
}
|
|
1106
|
-
if (!record$
|
|
1106
|
+
if (!record$6(parsed) || parsed["version"] !== 2 || !Array.isArray(parsed["updates"])) {
|
|
1107
1107
|
add("Dependabot must declare version: 2 and an updates array");
|
|
1108
1108
|
return diagnostics;
|
|
1109
1109
|
}
|
|
@@ -1116,7 +1116,7 @@ const checkDependabot = ({ files, policy }) => {
|
|
|
1116
1116
|
});
|
|
1117
1117
|
if (legacyDocument.errors.length === 0) legacy = legacyDocument.toJS({ maxAliasCount: 100 });
|
|
1118
1118
|
} catch {}
|
|
1119
|
-
const legacyUpdates = record$
|
|
1119
|
+
const legacyUpdates = record$6(legacy) && Array.isArray(legacy["updates"]) ? legacy["updates"] : [];
|
|
1120
1120
|
const rootClaims = /* @__PURE__ */ new Map();
|
|
1121
1121
|
const directoryClaims = /* @__PURE__ */ new Map();
|
|
1122
1122
|
for (const [index, update] of parsed["updates"].entries()) {
|
|
@@ -1130,7 +1130,7 @@ const checkDependabot = ({ files, policy }) => {
|
|
|
1130
1130
|
return offset === void 0 ? 1 : lineCounter.linePos(offset).line;
|
|
1131
1131
|
};
|
|
1132
1132
|
const report = (key, message) => add(message, lineOf(key));
|
|
1133
|
-
if (!record$
|
|
1133
|
+
if (!record$6(update) || typeof update["package-ecosystem"] !== "string") {
|
|
1134
1134
|
report("package-ecosystem", "each Dependabot update must declare a package ecosystem");
|
|
1135
1135
|
continue;
|
|
1136
1136
|
}
|
|
@@ -1164,26 +1164,75 @@ const checkDependabot = ({ files, policy }) => {
|
|
|
1164
1164
|
covered.set(ecosystem, matches);
|
|
1165
1165
|
const schedule = update["schedule"];
|
|
1166
1166
|
const legacyUpdate = legacyUpdates.at(index);
|
|
1167
|
-
const legacySchedule = record$
|
|
1168
|
-
if (record$
|
|
1169
|
-
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`);
|
|
1170
1170
|
const cooldown = update["cooldown"];
|
|
1171
|
-
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}`);
|
|
1172
1172
|
const groups = update["groups"];
|
|
1173
|
-
if (!record$
|
|
1173
|
+
if (!record$6(groups) || Object.keys(groups).length !== Object.keys(policy.groups).length || Object.entries(policy.groups).some(([name, expected]) => {
|
|
1174
1174
|
const group = groups[name];
|
|
1175
|
-
return !record$
|
|
1175
|
+
return !record$6(group) || Object.keys(group).length !== 2 || !sameStrings(group["patterns"], expected.patterns) || !sameStrings(group["update-types"], expected.updateTypes);
|
|
1176
1176
|
})) report("groups", `${ecosystem}: groups must match the shared policy`);
|
|
1177
1177
|
const ignored = ignoredFor(ecosystem, policy);
|
|
1178
1178
|
const ignore = update["ignore"];
|
|
1179
1179
|
if (ignore !== void 0 && (!Array.isArray(ignore) || ignore.length === 0)) report("ignore", `${ecosystem}: omit ignore or provide a nonempty array`);
|
|
1180
|
-
if (Array.isArray(ignore) && ignore.some((entry) => !record$
|
|
1181
|
-
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}`);
|
|
1182
1182
|
}
|
|
1183
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}`);
|
|
1184
1184
|
return diagnostics;
|
|
1185
1185
|
};
|
|
1186
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
|
|
1187
1236
|
//#region src/toolchain-engine-floors.ts
|
|
1188
1237
|
const record$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1189
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 !== "..");
|
|
@@ -1995,7 +2044,7 @@ const pythonRequirementMatches = (requirement, version) => {
|
|
|
1995
2044
|
});
|
|
1996
2045
|
};
|
|
1997
2046
|
/** Check runtime declarations only in the files that declare or configure them. */
|
|
1998
|
-
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 }) => {
|
|
1999
2048
|
const diagnostics = [];
|
|
2000
2049
|
const name = node_path.default.posix.basename(file);
|
|
2001
2050
|
const lines = text.split(/\r?\n/);
|
|
@@ -2007,6 +2056,16 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2007
2056
|
message
|
|
2008
2057
|
});
|
|
2009
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
|
+
};
|
|
2010
2069
|
const lineOf = (key) => Math.max(1, lines.findIndex((line) => line.includes(key)) + 1);
|
|
2011
2070
|
const pin = ({ rule, value, expected, label, line = 1, selector }) => {
|
|
2012
2071
|
let matches = value === expected;
|
|
@@ -2208,12 +2267,13 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2208
2267
|
line: index + 1
|
|
2209
2268
|
});
|
|
2210
2269
|
});
|
|
2211
|
-
const checkRuntimeImage = ({ image, line, label }) => {
|
|
2270
|
+
const checkRuntimeImage = ({ image, line, label, at }) => {
|
|
2212
2271
|
const unknownRuntime = () => {
|
|
2272
|
+
if (dynamic(image) && authorizeDynamic("image", line, at)) return;
|
|
2213
2273
|
add({
|
|
2214
2274
|
rule: "runtime-docker",
|
|
2215
2275
|
line,
|
|
2216
|
-
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`
|
|
2217
2277
|
});
|
|
2218
2278
|
};
|
|
2219
2279
|
if (typeof image !== "string") {
|
|
@@ -2442,6 +2502,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2442
2502
|
checkRuntimeImage({
|
|
2443
2503
|
image: entry.image,
|
|
2444
2504
|
line,
|
|
2505
|
+
at: entry.path.join("."),
|
|
2445
2506
|
label: classified?.ecosystem === "docker-compose" ? "Compose image" : "Kubernetes image"
|
|
2446
2507
|
});
|
|
2447
2508
|
}
|
|
@@ -2476,8 +2537,22 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2476
2537
|
let checkoutBindings = [];
|
|
2477
2538
|
let checkoutDestinations = /* @__PURE__ */ new Map();
|
|
2478
2539
|
let unknownCheckoutDestination = false;
|
|
2540
|
+
let unreviewableCheckoutSource = false;
|
|
2479
2541
|
const staticRepositoryPath = (value) => typeof value === "string" && value !== "" && !value.includes("${{") && !value.includes("\\") && !value.includes(":") && !node_path.default.posix.isAbsolute(value) && !value.split("/").includes("..");
|
|
2480
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
|
+
};
|
|
2481
2556
|
const checkReference = ({ value, tool, line }) => {
|
|
2482
2557
|
const allowed = {
|
|
2483
2558
|
node: [".node-version", ".nvmrc"],
|
|
@@ -2486,8 +2561,28 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2486
2561
|
}[tool];
|
|
2487
2562
|
const rule = tool === "bun" ? "bun-pins" : "runtime-workflow";
|
|
2488
2563
|
const selector = staticRepositoryPath(value) ? node_path.default.posix.normalize(value) : void 0;
|
|
2489
|
-
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
|
+
}
|
|
2490
2584
|
if (selector === void 0) {
|
|
2585
|
+
if (tool === "bun" && dynamic(value) && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
|
|
2491
2586
|
add({
|
|
2492
2587
|
rule,
|
|
2493
2588
|
line,
|
|
@@ -2495,7 +2590,8 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2495
2590
|
});
|
|
2496
2591
|
return;
|
|
2497
2592
|
}
|
|
2498
|
-
if (unknownCheckoutDestination ||
|
|
2593
|
+
if (unknownCheckoutDestination || hasAmbiguousCheckoutWriters(selector, providers)) {
|
|
2594
|
+
if (tool === "bun" && dynamicSource && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
|
|
2499
2595
|
add({
|
|
2500
2596
|
rule,
|
|
2501
2597
|
line,
|
|
@@ -2504,6 +2600,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2504
2600
|
return;
|
|
2505
2601
|
}
|
|
2506
2602
|
if (binding?.source === "untrusted") {
|
|
2603
|
+
if (tool === "bun" && dynamicSource && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
|
|
2507
2604
|
add({
|
|
2508
2605
|
rule,
|
|
2509
2606
|
line: binding.line,
|
|
@@ -2520,13 +2617,15 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2520
2617
|
});
|
|
2521
2618
|
return;
|
|
2522
2619
|
}
|
|
2523
|
-
if (binding !== void 0 && (binding.sparse.mode === "
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
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
|
+
}
|
|
2530
2629
|
}
|
|
2531
2630
|
if (binding?.source === "delegated") {
|
|
2532
2631
|
onDelegated?.({
|
|
@@ -2648,10 +2747,10 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2648
2747
|
return inputs;
|
|
2649
2748
|
};
|
|
2650
2749
|
const getInput = (node, key) => inputsOf(node).get(key.toUpperCase());
|
|
2651
|
-
const
|
|
2652
|
-
if (
|
|
2750
|
+
const expressionAt = (value, start) => {
|
|
2751
|
+
if (!value.startsWith("${{", start)) return void 0;
|
|
2653
2752
|
let quote;
|
|
2654
|
-
for (let index = 3; index < value.length; index++) {
|
|
2753
|
+
for (let index = start + 3; index < value.length; index++) {
|
|
2655
2754
|
const character = value.at(index);
|
|
2656
2755
|
if (quote !== void 0) {
|
|
2657
2756
|
if (character === quote) if (quote === "'" && value.at(index + 1) === "'") index++;
|
|
@@ -2661,9 +2760,33 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2661
2760
|
}
|
|
2662
2761
|
if (character === "'" || character === "\"") quote = character;
|
|
2663
2762
|
else if (value.startsWith("${{", index)) return void 0;
|
|
2664
|
-
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
|
+
};
|
|
2665
2767
|
}
|
|
2666
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
|
+
};
|
|
2667
2790
|
const checkoutPrefixOf = (node) => {
|
|
2668
2791
|
const destination = getInput(node, "path");
|
|
2669
2792
|
if (destination === void 0) return ".";
|
|
@@ -2676,11 +2799,19 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2676
2799
|
const value = getInput(node, "sparse-checkout");
|
|
2677
2800
|
const cone = getInput(node, "sparse-checkout-cone-mode");
|
|
2678
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" };
|
|
2679
|
-
const paths = value.value.split(/\r?\n/).map((entry) => entry.trim()).filter((entry) => entry !== "");
|
|
2680
|
-
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
|
+
}
|
|
2681
2812
|
return {
|
|
2682
2813
|
mode: "files",
|
|
2683
|
-
paths:
|
|
2814
|
+
paths: literalPaths
|
|
2684
2815
|
};
|
|
2685
2816
|
};
|
|
2686
2817
|
let currentEngineFloor;
|
|
@@ -2710,8 +2841,9 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2710
2841
|
}
|
|
2711
2842
|
if (value.startsWith("./")) {
|
|
2712
2843
|
const selector = staticRepositoryPath(value) ? node_path.default.posix.normalize(value) : void 0;
|
|
2713
|
-
const
|
|
2714
|
-
|
|
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({
|
|
2715
2847
|
rule: "action-pins",
|
|
2716
2848
|
line,
|
|
2717
2849
|
message: "local actions require an unambiguous checkout of the current source snapshot"
|
|
@@ -2767,24 +2899,55 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2767
2899
|
const checkoutRepository = getInput(node, "repository");
|
|
2768
2900
|
const checkoutRef = getInput(node, "ref");
|
|
2769
2901
|
const prefix = checkoutPrefixOf(node);
|
|
2902
|
+
const clean = getInput(node, "clean");
|
|
2903
|
+
const cleanIsResolved = clean === void 0 || (0, yaml.isScalar)(clean) && !dynamic(clean.value);
|
|
2770
2904
|
let repo;
|
|
2771
2905
|
if (checkoutRepository !== void 0) repo = (0, yaml.isScalar)(checkoutRepository) ? checkoutRepository.value : null;
|
|
2772
2906
|
const self = repo === void 0 || repo === "${{ github.repository }}" || typeof repo === "string" && repository !== void 0 && repo.toLowerCase() === repository.toLowerCase();
|
|
2773
2907
|
const workflowSource = repo === "${{ job.workflow_repository }}" && (0, yaml.isScalar)(checkoutRef) && checkoutRef.value === "${{ job.workflow_sha }}";
|
|
2774
|
-
const
|
|
2775
|
-
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");
|
|
2776
2919
|
const dynamicRef = dynamicRefBody(refValue);
|
|
2777
|
-
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;
|
|
2778
2929
|
if (!staticRepositoryPath(prefix)) checkoutBindings.push({
|
|
2779
2930
|
path: ".",
|
|
2931
|
+
dynamicSource: validSource && dynamic(prefix),
|
|
2780
2932
|
source: "untrusted",
|
|
2781
2933
|
reason: "checkout destination must be a static repository path",
|
|
2782
2934
|
line,
|
|
2783
|
-
sparse:
|
|
2935
|
+
sparse: sparseCheckoutOf(node)
|
|
2784
2936
|
});
|
|
2785
2937
|
else {
|
|
2786
2938
|
const destination = normalizeCheckoutPath(prefix);
|
|
2787
|
-
|
|
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({
|
|
2788
2951
|
node,
|
|
2789
2952
|
key: "if"
|
|
2790
2953
|
}) === void 0 && sourceMap({
|
|
@@ -2793,18 +2956,21 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2793
2956
|
}) === void 0 && checkoutDestinations.get(destination) === 1 && !unknownCheckoutDestination;
|
|
2794
2957
|
if (trustedWriter && delegatedSource && typeof refValue === "string") checkoutBindings.push({
|
|
2795
2958
|
path: destination,
|
|
2959
|
+
dynamicSource: validSource && delegatedSource,
|
|
2796
2960
|
sparse: sparseCheckoutOf(node),
|
|
2797
2961
|
source: "delegated",
|
|
2798
2962
|
ref: refValue
|
|
2799
2963
|
});
|
|
2800
2964
|
else if (trustedWriter && (workflowSource || currentSource)) checkoutBindings.push({
|
|
2801
2965
|
path: destination,
|
|
2966
|
+
dynamicSource: validSource && delegatedSource,
|
|
2802
2967
|
sparse: sparseCheckoutOf(node),
|
|
2803
2968
|
source: "tracked"
|
|
2804
2969
|
});
|
|
2805
2970
|
else {
|
|
2806
2971
|
let reason = "checkout does not select a trusted repository source snapshot";
|
|
2807
|
-
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";
|
|
2808
2974
|
else if (sourceMap({
|
|
2809
2975
|
node,
|
|
2810
2976
|
key: "if"
|
|
@@ -2815,6 +2981,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2815
2981
|
else if (checkoutDestinations.get(destination) !== 1 || unknownCheckoutDestination) reason = "checkout destination must have exactly one static writer";
|
|
2816
2982
|
checkoutBindings.push({
|
|
2817
2983
|
path: destination,
|
|
2984
|
+
dynamicSource: validSource && delegatedSource,
|
|
2818
2985
|
sparse: sparseCheckoutOf(node),
|
|
2819
2986
|
source: "untrusted",
|
|
2820
2987
|
reason,
|
|
@@ -2863,21 +3030,23 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2863
3030
|
line: reference === void 0 ? line : nodeLine(reference)
|
|
2864
3031
|
});
|
|
2865
3032
|
};
|
|
2866
|
-
const checkContainer = (node) => {
|
|
3033
|
+
const checkContainer = (node, at) => {
|
|
2867
3034
|
node = resolveNode(node);
|
|
2868
3035
|
if (node === void 0) return;
|
|
2869
3036
|
const image = (0, yaml.isMap)(node) ? getNode(node, "image") : node;
|
|
2870
3037
|
checkRuntimeImage({
|
|
2871
3038
|
image: (0, yaml.isScalar)(image) ? image.value : void 0,
|
|
2872
3039
|
line: nodeLine(image ?? node),
|
|
2873
|
-
label: "container image"
|
|
3040
|
+
label: "container image",
|
|
3041
|
+
at
|
|
2874
3042
|
});
|
|
2875
3043
|
};
|
|
2876
|
-
const checkSteps = (node, floor) => {
|
|
3044
|
+
const checkSteps = (node, locationPrefix, floor) => {
|
|
2877
3045
|
currentEngineFloor = void 0;
|
|
2878
3046
|
checkoutBindings = [];
|
|
2879
3047
|
checkoutDestinations = /* @__PURE__ */ new Map();
|
|
2880
3048
|
unknownCheckoutDestination = false;
|
|
3049
|
+
unreviewableCheckoutSource = false;
|
|
2881
3050
|
node = resolveNode(node);
|
|
2882
3051
|
if (!(0, yaml.isSeq)(node)) return;
|
|
2883
3052
|
for (const step of node.items) {
|
|
@@ -2893,9 +3062,14 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2893
3062
|
}
|
|
2894
3063
|
currentEngineFloor = floor;
|
|
2895
3064
|
try {
|
|
2896
|
-
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
|
+
}
|
|
2897
3070
|
} finally {
|
|
2898
3071
|
currentEngineFloor = void 0;
|
|
3072
|
+
currentLocation = void 0;
|
|
2899
3073
|
}
|
|
2900
3074
|
};
|
|
2901
3075
|
try {
|
|
@@ -2909,15 +3083,15 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
|
|
|
2909
3083
|
currentEngineFloor = void 0;
|
|
2910
3084
|
const job = getNode(jobs, key);
|
|
2911
3085
|
checkAction(job);
|
|
2912
|
-
checkContainer(getNode(job, "container"));
|
|
3086
|
+
checkContainer(getNode(job, "container"), `jobs.${key}.container`);
|
|
2913
3087
|
const services = getNode(job, "services");
|
|
2914
|
-
if ((0, yaml.isMap)(services)) for (const service of keysOf(services)) checkContainer(getNode(services, service));
|
|
2915
|
-
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));
|
|
2916
3090
|
}
|
|
2917
3091
|
} else {
|
|
2918
3092
|
const runs = getNode(document.contents, "runs");
|
|
2919
3093
|
const using = getNode(runs, "using");
|
|
2920
|
-
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");
|
|
2921
3095
|
if ((0, yaml.isScalar)(using) && using.value === "docker") {
|
|
2922
3096
|
const image = getNode(runs, "image");
|
|
2923
3097
|
const value = (0, yaml.isScalar)(image) ? image.value : void 0;
|
|
@@ -3146,7 +3320,7 @@ const record = (value) => typeof value === "object" && value !== null && !Array.
|
|
|
3146
3320
|
const array = (value) => Array.isArray(value);
|
|
3147
3321
|
/** Repository-owned declarations are explicit; malformed decisions fail closed. */
|
|
3148
3322
|
const parseToolchainConfiguration = (input) => {
|
|
3149
|
-
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");
|
|
3150
3324
|
const disabled = /* @__PURE__ */ new Set();
|
|
3151
3325
|
const optOuts = input["optOuts"] ?? [];
|
|
3152
3326
|
if (!array(optOuts)) throw new Error("optOuts must be an array");
|
|
@@ -3159,7 +3333,8 @@ const parseToolchainConfiguration = (input) => {
|
|
|
3159
3333
|
return {
|
|
3160
3334
|
disabled,
|
|
3161
3335
|
cloud: parseCloudSetup(input["cloud"]),
|
|
3162
|
-
engineFloors: parseEngineFloors(input["engineFloors"])
|
|
3336
|
+
engineFloors: parseEngineFloors(input["engineFloors"]),
|
|
3337
|
+
dynamicSelectors: parseDynamicSelectors(input["dynamicSelectors"])
|
|
3163
3338
|
};
|
|
3164
3339
|
};
|
|
3165
3340
|
/** Read one tracked configuration snapshot for guards and policy generation. */
|
|
@@ -3225,10 +3400,12 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3225
3400
|
let disabled = /* @__PURE__ */ new Set();
|
|
3226
3401
|
let cloud;
|
|
3227
3402
|
const engineFloors = [];
|
|
3403
|
+
let dynamicSelectors = [];
|
|
3228
3404
|
if (files["stll-toolchain.json"] !== void 0) try {
|
|
3229
3405
|
const configuration = parseToolchainConfiguration(JSON.parse(files["stll-toolchain.json"]));
|
|
3230
3406
|
disabled = configuration.disabled;
|
|
3231
3407
|
cloud = configuration.cloud;
|
|
3408
|
+
dynamicSelectors = configuration.dynamicSelectors;
|
|
3232
3409
|
for (const entry of configuration.engineFloors) engineFloors.push(resolveEngineFloor(entry, files));
|
|
3233
3410
|
} catch (error) {
|
|
3234
3411
|
diagnostics.push({
|
|
@@ -3268,6 +3445,7 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3268
3445
|
}
|
|
3269
3446
|
const exercisedFloors = /* @__PURE__ */ new Set();
|
|
3270
3447
|
const mismatchedFloors = /* @__PURE__ */ new Set();
|
|
3448
|
+
const selectorMatches = /* @__PURE__ */ new Map();
|
|
3271
3449
|
diagnostics.push(...checkPackageFiles({
|
|
3272
3450
|
files,
|
|
3273
3451
|
policy
|
|
@@ -3283,6 +3461,8 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3283
3461
|
}
|
|
3284
3462
|
},
|
|
3285
3463
|
trackedFiles,
|
|
3464
|
+
dynamicSelectors: dynamicSelectors.filter((entry) => entry.path === file),
|
|
3465
|
+
onDynamicSelector: (entry) => selectorMatches.set(entry, (selectorMatches.get(entry) ?? 0) + 1),
|
|
3286
3466
|
repository,
|
|
3287
3467
|
onDelegated,
|
|
3288
3468
|
engineFloors: engineFloors.filter((entry) => entry.workflow === file),
|
|
@@ -3298,6 +3478,12 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
|
|
|
3298
3478
|
line: 1,
|
|
3299
3479
|
message: `engine floor ${entry.workflow}:${entry.job} requires a setup-node exact patch satisfying ${entry.package} engines.node minimum major ${entry.major}`
|
|
3300
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
|
+
});
|
|
3301
3487
|
diagnostics.push(...checkDependabot({
|
|
3302
3488
|
files,
|
|
3303
3489
|
policy: policy.dependabot
|