workspace-conformance 1.5.0 → 1.7.0
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 +8 -1
- package/dist/cli.js +395 -148
- package/dist/index.cjs +397 -148
- package/dist/index.d.cts +53 -1
- package/dist/index.d.ts +53 -1
- package/dist/index.js +395 -148
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { escapePath, glob, isDynamicPattern } from "tinyglobby";
|
|
|
5
5
|
import { ModuleKind, ModuleResolutionKind, Node, Project, ScriptTarget, SyntaxKind, ts } from "ts-morph";
|
|
6
6
|
import { createJitiLoader, validateStandard } from "cosmiconfig-extends";
|
|
7
7
|
import { readFile, realpath } from "node:fs/promises";
|
|
8
|
+
import { Range, subset, valid, validRange } from "semver";
|
|
8
9
|
import { LineCounter, isMap, isNode, isScalar, isSeq, parse, parseDocument } from "yaml";
|
|
9
10
|
import { execFile } from "node:child_process";
|
|
10
11
|
import { findNodeAtLocation, parse as parse$1, parseTree, printParseErrorCode } from "jsonc-parser";
|
|
@@ -781,6 +782,301 @@ const dockerfilePackageManager = async ({ cwd, options }) => {
|
|
|
781
782
|
return violations;
|
|
782
783
|
};
|
|
783
784
|
//#endregion
|
|
785
|
+
//#region src/workspace/packages.ts
|
|
786
|
+
function segmentsOf(path) {
|
|
787
|
+
return path.split("/").filter((segment) => segment !== "" && segment !== ".");
|
|
788
|
+
}
|
|
789
|
+
function groupRoot(group) {
|
|
790
|
+
return segmentsOf(group.path ?? group.name);
|
|
791
|
+
}
|
|
792
|
+
function startsWithSegments(segments, prefix) {
|
|
793
|
+
return prefix.every((segment, index) => segments[index] === segment);
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* The group whose root is the longest prefix of `dir`; the first declared wins a tie. `undefined` when no group owns the directory.
|
|
797
|
+
*/
|
|
798
|
+
function owningGroup(dir, groups) {
|
|
799
|
+
const segments = segmentsOf(dir);
|
|
800
|
+
let best;
|
|
801
|
+
let bestLength = -1;
|
|
802
|
+
for (const group of groups) {
|
|
803
|
+
const root = groupRoot(group);
|
|
804
|
+
if (root.length > bestLength && startsWithSegments(segments, root)) {
|
|
805
|
+
best = group;
|
|
806
|
+
bestLength = root.length;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return best;
|
|
810
|
+
}
|
|
811
|
+
function rankOf(name, group, layout) {
|
|
812
|
+
if (name !== void 0) {
|
|
813
|
+
const rule = layout.nameRanks?.find((candidate) => new RegExp(candidate.pattern, "u").test(name));
|
|
814
|
+
if (rule !== void 0) return rule.rank;
|
|
815
|
+
}
|
|
816
|
+
return group.rank ?? layout.defaultRank;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* The name without its npm scope: `@scope/store-cli` gives `store-cli`.
|
|
820
|
+
*/
|
|
821
|
+
function unscoped(name) {
|
|
822
|
+
return name.replace(/^@[^/]+\//u, "");
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* The longest slice value that is the whole unscoped name or its prefix up to a `-`.
|
|
826
|
+
*/
|
|
827
|
+
function sliceByNamePrefix(name, known) {
|
|
828
|
+
const bare = unscoped(name);
|
|
829
|
+
return [...known].filter((candidate) => bare === candidate || bare.startsWith(`${candidate}-`)).sort((a, b) => b.length - a.length)[0];
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Classify discovered packages by the layout: group, rank and slice. Pure.
|
|
833
|
+
*
|
|
834
|
+
* A package no group owns is a configuration error, because every check would otherwise skip it without saying so. Nested package directories are one too: the checks attribute a file to the package whose directory is its prefix, which is ambiguous for a package inside another. A `namePrefix` slice takes the longest slice value that a `segment` group produced and that prefixes the package's unscoped name; a package without a name has no such slice.
|
|
835
|
+
*/
|
|
836
|
+
function classifyPackages(discovered, layout) {
|
|
837
|
+
const owned = discovered.map((found) => {
|
|
838
|
+
const group = owningGroup(found.dir, layout.groups);
|
|
839
|
+
if (group === void 0) throw new ConformanceError(`the package directory '${found.dir}' is not under any layout group's path; add a group whose path prefixes it, or narrow the workspace package globs`);
|
|
840
|
+
return {
|
|
841
|
+
found,
|
|
842
|
+
group
|
|
843
|
+
};
|
|
844
|
+
});
|
|
845
|
+
const knownSlices = /* @__PURE__ */ new Set();
|
|
846
|
+
for (const { found, group } of owned) if (group.slice !== void 0 && "segment" in group.slice) {
|
|
847
|
+
const value = segmentsOf(found.dir).slice(groupRoot(group).length)[group.slice.segment];
|
|
848
|
+
if (value !== void 0) knownSlices.add(value);
|
|
849
|
+
}
|
|
850
|
+
const packages = owned.map(({ found, group }) => {
|
|
851
|
+
let slice;
|
|
852
|
+
if (group.slice !== void 0 && "segment" in group.slice) slice = segmentsOf(found.dir).slice(groupRoot(group).length)[group.slice.segment];
|
|
853
|
+
else if (group.slice !== void 0 && found.name !== void 0) slice = sliceByNamePrefix(found.name, knownSlices);
|
|
854
|
+
return {
|
|
855
|
+
...found,
|
|
856
|
+
group: group.name,
|
|
857
|
+
rank: rankOf(found.name, group, layout),
|
|
858
|
+
slice
|
|
859
|
+
};
|
|
860
|
+
});
|
|
861
|
+
for (const outer of packages) {
|
|
862
|
+
const nested = packages.find((inner) => inner.dir !== outer.dir && inner.dir.startsWith(`${outer.dir}/`));
|
|
863
|
+
if (nested !== void 0) throw new ConformanceError(`the package directory '${nested.dir}' is inside the package directory '${outer.dir}'; workspace packages must not nest`);
|
|
864
|
+
}
|
|
865
|
+
return packages;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* The workspace root: `layout.root` resolved against `cwd`, or `cwd` itself.
|
|
869
|
+
*/
|
|
870
|
+
function workspaceRoot(cwd, layout) {
|
|
871
|
+
return layout.root === void 0 ? cwd : resolve(cwd, layout.root);
|
|
872
|
+
}
|
|
873
|
+
const PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
|
|
874
|
+
/**
|
|
875
|
+
* The `packages` list of `pnpm-workspace.yaml` in `root`, or `undefined` when there is no such file. A file that exists but cannot be read, or has no list of strings under `packages`, is a configuration error.
|
|
876
|
+
*/
|
|
877
|
+
async function pnpmWorkspacePatterns(root) {
|
|
878
|
+
const file = join(root, PNPM_WORKSPACE_FILE);
|
|
879
|
+
if (!existsSync(file)) return;
|
|
880
|
+
let source;
|
|
881
|
+
try {
|
|
882
|
+
source = await readFile(file, "utf8");
|
|
883
|
+
} catch (error) {
|
|
884
|
+
throw new ConformanceError(`${file} cannot be read`, { cause: error });
|
|
885
|
+
}
|
|
886
|
+
const document = parse(source);
|
|
887
|
+
if (typeof document !== "object" || document === null || !("packages" in document) || !Array.isArray(document.packages)) throw new ConformanceError(`${file} has no 'packages' list`);
|
|
888
|
+
const patterns = document.packages;
|
|
889
|
+
if (!patterns.every((pattern) => typeof pattern === "string")) throw new ConformanceError(`${file}: every entry of 'packages' must be a string`);
|
|
890
|
+
return patterns;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* The package globs: `layout.packages`, else the `packages` list of `pnpm-workspace.yaml` in `root`.
|
|
894
|
+
*/
|
|
895
|
+
async function workspacePatterns(root, layout) {
|
|
896
|
+
if (layout.packages !== void 0) return layout.packages;
|
|
897
|
+
const patterns = await pnpmWorkspacePatterns(root);
|
|
898
|
+
if (patterns === void 0) throw new ConformanceError(`the layout has no 'packages' and ${join(root, PNPM_WORKSPACE_FILE)} does not exist`);
|
|
899
|
+
return patterns;
|
|
900
|
+
}
|
|
901
|
+
async function readName(manifest) {
|
|
902
|
+
let content;
|
|
903
|
+
try {
|
|
904
|
+
content = JSON.parse(await readFile(manifest, "utf8"));
|
|
905
|
+
} catch (error) {
|
|
906
|
+
throw new ConformanceError(`${manifest} cannot be read as JSON`, { cause: error });
|
|
907
|
+
}
|
|
908
|
+
if (typeof content === "object" && content !== null && "name" in content && typeof content.name === "string") return content.name;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* The directories under `root` that the pnpm-style `patterns` select and that hold a `package.json`, sorted. A `!` pattern excludes. The root package itself (`.`) is not a member of any group and is left out.
|
|
912
|
+
*/
|
|
913
|
+
async function discoverPackages(root, patterns) {
|
|
914
|
+
const manifests = patterns.map((pattern) => pattern.startsWith("!") ? `!${pattern.slice(1)}/**` : `${pattern}/package.json`);
|
|
915
|
+
const dirs = (await glob(manifests, {
|
|
916
|
+
cwd: root,
|
|
917
|
+
dot: true,
|
|
918
|
+
ignore: [INSTALLED_DEPENDENCIES_GLOB],
|
|
919
|
+
onlyFiles: true
|
|
920
|
+
})).map((manifest) => manifest.replace(/\/?package\.json$/u, "")).filter((dir) => dir !== "");
|
|
921
|
+
return Promise.all([...new Set(dirs)].sort().map(async (dir) => ({
|
|
922
|
+
dir,
|
|
923
|
+
name: await readName(join(root, dir, "package.json"))
|
|
924
|
+
})));
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* The layout's packages found under the workspace root.
|
|
928
|
+
*/
|
|
929
|
+
async function readWorkspacePackages(cwd, layout) {
|
|
930
|
+
const root = workspaceRoot(cwd, layout);
|
|
931
|
+
return classifyPackages(await discoverPackages(root, await workspacePatterns(root, layout)), layout);
|
|
932
|
+
}
|
|
933
|
+
//#endregion
|
|
934
|
+
//#region src/checks/engines-floor.ts
|
|
935
|
+
/**
|
|
936
|
+
* The pattern that selects the root package, the `package.json` in the directory the checks run in.
|
|
937
|
+
*/
|
|
938
|
+
const ROOT_PACKAGE = ".";
|
|
939
|
+
const MANIFEST = "package.json";
|
|
940
|
+
const ANY_INSTALLED_COPY = {};
|
|
941
|
+
const DEPENDENCY_FIELDS = [
|
|
942
|
+
{
|
|
943
|
+
field: "dependencies",
|
|
944
|
+
noun: "dependency",
|
|
945
|
+
mayBeAbsent: () => false,
|
|
946
|
+
judgedCopy: () => ANY_INSTALLED_COPY
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
field: "optionalDependencies",
|
|
950
|
+
noun: "optional dependency",
|
|
951
|
+
mayBeAbsent: () => true,
|
|
952
|
+
judgedCopy: () => ANY_INSTALLED_COPY
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
field: "peerDependencies",
|
|
956
|
+
noun: "peer dependency",
|
|
957
|
+
mayBeAbsent: (manifest, name) => {
|
|
958
|
+
const meta = manifest["peerDependenciesMeta"];
|
|
959
|
+
const entry = isRecord(meta) ? meta[name] : void 0;
|
|
960
|
+
return isRecord(entry) && entry["optional"] === true;
|
|
961
|
+
},
|
|
962
|
+
judgedCopy: (spec) => {
|
|
963
|
+
const version = onlyAdmittedVersion(spec);
|
|
964
|
+
return version === null ? void 0 : { version };
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
];
|
|
968
|
+
/**
|
|
969
|
+
* The one version `spec` admits, when it is a semver range of a single comparator naming an exact version (`1.2.3` or `=1.2.3`); `null`, as semver gives for no version, for any other range, including one of a single major, and for a spec that is not a semver range (`workspace:^`, a tag or a URL).
|
|
970
|
+
*/
|
|
971
|
+
function onlyAdmittedVersion(spec) {
|
|
972
|
+
if (validRange(spec) === null) return null;
|
|
973
|
+
const [comparators, ...others] = new Range(spec).set;
|
|
974
|
+
if (comparators === void 0 || others.length > 0 || comparators.length !== 1) return null;
|
|
975
|
+
const [comparator] = comparators;
|
|
976
|
+
if (comparator?.operator !== "") return null;
|
|
977
|
+
return valid(comparator.value);
|
|
978
|
+
}
|
|
979
|
+
async function readManifest(cwd, file) {
|
|
980
|
+
let content;
|
|
981
|
+
try {
|
|
982
|
+
content = JSON.parse(await readFile(resolve(cwd, file), "utf8"));
|
|
983
|
+
} catch (error) {
|
|
984
|
+
throw new ConformanceError(`${file} cannot be read as JSON`, { cause: error });
|
|
985
|
+
}
|
|
986
|
+
if (!isRecord(content)) throw new ConformanceError(`${file} is not a JSON object`);
|
|
987
|
+
return content;
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* The `engines.node` range the manifest at `file` declares, or `undefined` when it declares none. A range semver cannot parse is a configuration error, since nothing can be said about the versions it admits.
|
|
991
|
+
*/
|
|
992
|
+
function nodeRange(manifest, file) {
|
|
993
|
+
const engines = manifest["engines"];
|
|
994
|
+
if (engines === void 0) return;
|
|
995
|
+
if (!isRecord(engines)) throw new ConformanceError(`${file}: engines must be an object`);
|
|
996
|
+
const node = engines["node"];
|
|
997
|
+
if (node === void 0) return;
|
|
998
|
+
if (typeof node !== "string") throw new ConformanceError(`${file}: engines.node must be a string, not ${JSON.stringify(node)}`);
|
|
999
|
+
if (validRange(node) === null) throw new ConformanceError(`${file}: engines.node '${node}' is not a valid semver range`);
|
|
1000
|
+
return node;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* The name and version spec of each dependency a dependency field of the manifest at `file` lists, sorted by name; none when the field is absent.
|
|
1004
|
+
*/
|
|
1005
|
+
function dependencyEntries(manifest, field, file) {
|
|
1006
|
+
const listed = manifest[field];
|
|
1007
|
+
if (listed === void 0) return [];
|
|
1008
|
+
if (!isRecord(listed)) throw new ConformanceError(`${file}: ${field} must be an object`);
|
|
1009
|
+
return Object.keys(listed).sort().map((name) => {
|
|
1010
|
+
const spec = listed[name];
|
|
1011
|
+
if (typeof spec !== "string") throw new ConformanceError(`${file}: ${field}.${name} must be a string, not ${JSON.stringify(spec)}`);
|
|
1012
|
+
return [name, spec];
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* The `package.json` of the dependency `name` as installed for the package in `packageDirectory`, found the way Node finds a package directory: in `node_modules` of the package's own directory, then of each directory above it, stopping at the working directory, since a copy above the workspace is not one its install put there. A symbolic link, as pnpm installs every dependency, is followed. `undefined` when no directory has it.
|
|
1017
|
+
*/
|
|
1018
|
+
function installedManifest(cwd, packageDirectory, name) {
|
|
1019
|
+
const top = resolve(cwd);
|
|
1020
|
+
for (let directory = resolve(cwd, packageDirectory);; directory = dirname(directory)) {
|
|
1021
|
+
const candidate = join(directory, "node_modules", name, MANIFEST);
|
|
1022
|
+
if (existsSync(candidate)) return relativePosix(cwd, candidate);
|
|
1023
|
+
if (directory === top) return;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* The directories of the packages to judge: those `packages` selects, else the root package and the `packages` of `pnpm-workspace.yaml`. Selecting none is a configuration error, so a pattern that matches nothing never reads as a pass.
|
|
1028
|
+
*/
|
|
1029
|
+
async function packageDirectories(cwd, options) {
|
|
1030
|
+
const patterns = options.packages ?? [ROOT_PACKAGE, ...await pnpmWorkspacePatterns(cwd) ?? []];
|
|
1031
|
+
const members = await discoverPackages(cwd, patterns.filter((pattern) => pattern !== ROOT_PACKAGE));
|
|
1032
|
+
const directories = [...patterns.includes(ROOT_PACKAGE) ? [ROOT_PACKAGE] : [], ...members.map((member) => member.dir)];
|
|
1033
|
+
if (directories.length === 0) throw new ConformanceError(`no package.json is selected by ${patterns.join(", ")}`);
|
|
1034
|
+
return directories;
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* The `engines.node` range of each package is a subset of the `engines.node` range of each dependency and optional dependency it lists, and of each peer dependency whose range admits a single version, as installed in `node_modules`, so the package admits no Node version that something it needs at run time rejects. A package that declares no range admits every version, so it is reported for each dependency that declares one. A dependency that declares no range constrains nothing and is ignored.
|
|
1038
|
+
*
|
|
1039
|
+
* The comparison is `semver.subset(package, dependency)`: every version the package's range admits satisfies the dependency's range. A floor alone is not enough, because a range of several lines (`^20.19.0 || ^22.13.0 || >=24`) also rejects the versions between them, which `>=20.19.0` admits. semver compares a range of several comparator sets set by set, so each set of the package's range must lie within one set of the dependency's; a set that only two adjacent dependency sets cover together (`>=22` against `^22.0.0 || >=23`) is reported, and splitting it the same way (`^22.0.0 || >=23`) satisfies the check.
|
|
1040
|
+
*
|
|
1041
|
+
* A peer dependency is judged only when its range admits a single version (`1.2.3` or `=1.2.3`), and then by the copy installed, which must be that version. The consumer chooses which admitted version to install, so the package's range holds when each Node version in it is accepted by some admitted version; one installed copy that rejects a Node version does not show that every admitted version does, and the others are not on disk.
|
|
1042
|
+
*
|
|
1043
|
+
* A dependency that is not installed fails the run: its range cannot be read, and passing it unjudged would read as a pass. An optional dependency and a peer dependency marked optional in `peerDependenciesMeta` may be absent and are then skipped.
|
|
1044
|
+
*/
|
|
1045
|
+
const enginesFloor = async ({ cwd, options }) => {
|
|
1046
|
+
const violations = [];
|
|
1047
|
+
for (const directory of await packageDirectories(cwd, options)) {
|
|
1048
|
+
const file = posix.join(directory, MANIFEST);
|
|
1049
|
+
const manifest = await readManifest(cwd, file);
|
|
1050
|
+
const own = nodeRange(manifest, file);
|
|
1051
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1052
|
+
for (const { field, noun, mayBeAbsent, judgedCopy } of DEPENDENCY_FIELDS) for (const [name, spec] of dependencyEntries(manifest, field, file).filter(([candidate]) => !seen.has(candidate))) {
|
|
1053
|
+
seen.add(name);
|
|
1054
|
+
const copy = judgedCopy(spec);
|
|
1055
|
+
if (copy === void 0) continue;
|
|
1056
|
+
const installed = installedManifest(cwd, directory, name);
|
|
1057
|
+
if (installed === void 0) {
|
|
1058
|
+
if (mayBeAbsent(manifest, name)) continue;
|
|
1059
|
+
throw new ConformanceError(`${file}: the ${noun} '${name}' is not installed (no node_modules/${name}/package.json in its directory or any directory above it up to the working directory); install the workspace before running engines-floor`);
|
|
1060
|
+
}
|
|
1061
|
+
const dependency = await readManifest(cwd, installed);
|
|
1062
|
+
if (copy.version !== void 0 && dependency["version"] !== copy.version) throw new ConformanceError(`${file}: the ${noun} '${name}' admits only version ${copy.version}, but ${installed} is version ${String(dependency["version"])}; install the workspace before running engines-floor`);
|
|
1063
|
+
const theirs = nodeRange(dependency, installed);
|
|
1064
|
+
if (theirs === void 0) continue;
|
|
1065
|
+
if (own === void 0) violations.push({
|
|
1066
|
+
code: "engines-floor/no-engines",
|
|
1067
|
+
message: `no engines.node is declared, so every Node version is admitted, but the ${noun} '${name}' requires engines.node '${theirs}'; declare a subset of that range`,
|
|
1068
|
+
file
|
|
1069
|
+
});
|
|
1070
|
+
else if (!subset(own, theirs)) violations.push({
|
|
1071
|
+
code: "engines-floor/wider-than-dependency",
|
|
1072
|
+
message: `engines.node '${own}' admits Node versions that the ${noun} '${name}' rejects with engines.node '${theirs}'; narrow it to a subset of that range`,
|
|
1073
|
+
file
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
return violations;
|
|
1078
|
+
};
|
|
1079
|
+
//#endregion
|
|
784
1080
|
//#region src/checks/eslint.ts
|
|
785
1081
|
/**
|
|
786
1082
|
* The patterns the full level lints when the options name none.
|
|
@@ -1109,145 +1405,6 @@ function unresolvedNameImportRule(packages) {
|
|
|
1109
1405
|
};
|
|
1110
1406
|
}
|
|
1111
1407
|
//#endregion
|
|
1112
|
-
//#region src/workspace/packages.ts
|
|
1113
|
-
function segmentsOf(path) {
|
|
1114
|
-
return path.split("/").filter((segment) => segment !== "" && segment !== ".");
|
|
1115
|
-
}
|
|
1116
|
-
function groupRoot(group) {
|
|
1117
|
-
return segmentsOf(group.path ?? group.name);
|
|
1118
|
-
}
|
|
1119
|
-
function startsWithSegments(segments, prefix) {
|
|
1120
|
-
return prefix.every((segment, index) => segments[index] === segment);
|
|
1121
|
-
}
|
|
1122
|
-
/**
|
|
1123
|
-
* The group whose root is the longest prefix of `dir`; the first declared wins a tie. `undefined` when no group owns the directory.
|
|
1124
|
-
*/
|
|
1125
|
-
function owningGroup(dir, groups) {
|
|
1126
|
-
const segments = segmentsOf(dir);
|
|
1127
|
-
let best;
|
|
1128
|
-
let bestLength = -1;
|
|
1129
|
-
for (const group of groups) {
|
|
1130
|
-
const root = groupRoot(group);
|
|
1131
|
-
if (root.length > bestLength && startsWithSegments(segments, root)) {
|
|
1132
|
-
best = group;
|
|
1133
|
-
bestLength = root.length;
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
return best;
|
|
1137
|
-
}
|
|
1138
|
-
function rankOf(name, group, layout) {
|
|
1139
|
-
if (name !== void 0) {
|
|
1140
|
-
const rule = layout.nameRanks?.find((candidate) => new RegExp(candidate.pattern, "u").test(name));
|
|
1141
|
-
if (rule !== void 0) return rule.rank;
|
|
1142
|
-
}
|
|
1143
|
-
return group.rank ?? layout.defaultRank;
|
|
1144
|
-
}
|
|
1145
|
-
/**
|
|
1146
|
-
* The name without its npm scope: `@scope/store-cli` gives `store-cli`.
|
|
1147
|
-
*/
|
|
1148
|
-
function unscoped(name) {
|
|
1149
|
-
return name.replace(/^@[^/]+\//u, "");
|
|
1150
|
-
}
|
|
1151
|
-
/**
|
|
1152
|
-
* The longest slice value that is the whole unscoped name or its prefix up to a `-`.
|
|
1153
|
-
*/
|
|
1154
|
-
function sliceByNamePrefix(name, known) {
|
|
1155
|
-
const bare = unscoped(name);
|
|
1156
|
-
return [...known].filter((candidate) => bare === candidate || bare.startsWith(`${candidate}-`)).sort((a, b) => b.length - a.length)[0];
|
|
1157
|
-
}
|
|
1158
|
-
/**
|
|
1159
|
-
* Classify discovered packages by the layout: group, rank and slice. Pure.
|
|
1160
|
-
*
|
|
1161
|
-
* A package no group owns is a configuration error, because every check would otherwise skip it without saying so. Nested package directories are one too: the checks attribute a file to the package whose directory is its prefix, which is ambiguous for a package inside another. A `namePrefix` slice takes the longest slice value that a `segment` group produced and that prefixes the package's unscoped name; a package without a name has no such slice.
|
|
1162
|
-
*/
|
|
1163
|
-
function classifyPackages(discovered, layout) {
|
|
1164
|
-
const owned = discovered.map((found) => {
|
|
1165
|
-
const group = owningGroup(found.dir, layout.groups);
|
|
1166
|
-
if (group === void 0) throw new ConformanceError(`the package directory '${found.dir}' is not under any layout group's path; add a group whose path prefixes it, or narrow the workspace package globs`);
|
|
1167
|
-
return {
|
|
1168
|
-
found,
|
|
1169
|
-
group
|
|
1170
|
-
};
|
|
1171
|
-
});
|
|
1172
|
-
const knownSlices = /* @__PURE__ */ new Set();
|
|
1173
|
-
for (const { found, group } of owned) if (group.slice !== void 0 && "segment" in group.slice) {
|
|
1174
|
-
const value = segmentsOf(found.dir).slice(groupRoot(group).length)[group.slice.segment];
|
|
1175
|
-
if (value !== void 0) knownSlices.add(value);
|
|
1176
|
-
}
|
|
1177
|
-
const packages = owned.map(({ found, group }) => {
|
|
1178
|
-
let slice;
|
|
1179
|
-
if (group.slice !== void 0 && "segment" in group.slice) slice = segmentsOf(found.dir).slice(groupRoot(group).length)[group.slice.segment];
|
|
1180
|
-
else if (group.slice !== void 0 && found.name !== void 0) slice = sliceByNamePrefix(found.name, knownSlices);
|
|
1181
|
-
return {
|
|
1182
|
-
...found,
|
|
1183
|
-
group: group.name,
|
|
1184
|
-
rank: rankOf(found.name, group, layout),
|
|
1185
|
-
slice
|
|
1186
|
-
};
|
|
1187
|
-
});
|
|
1188
|
-
for (const outer of packages) {
|
|
1189
|
-
const nested = packages.find((inner) => inner.dir !== outer.dir && inner.dir.startsWith(`${outer.dir}/`));
|
|
1190
|
-
if (nested !== void 0) throw new ConformanceError(`the package directory '${nested.dir}' is inside the package directory '${outer.dir}'; workspace packages must not nest`);
|
|
1191
|
-
}
|
|
1192
|
-
return packages;
|
|
1193
|
-
}
|
|
1194
|
-
/**
|
|
1195
|
-
* The workspace root: `layout.root` resolved against `cwd`, or `cwd` itself.
|
|
1196
|
-
*/
|
|
1197
|
-
function workspaceRoot(cwd, layout) {
|
|
1198
|
-
return layout.root === void 0 ? cwd : resolve(cwd, layout.root);
|
|
1199
|
-
}
|
|
1200
|
-
/**
|
|
1201
|
-
* The package globs: `layout.packages`, else the `packages` list of `pnpm-workspace.yaml` in `root`.
|
|
1202
|
-
*/
|
|
1203
|
-
async function workspacePatterns(root, layout) {
|
|
1204
|
-
if (layout.packages !== void 0) return layout.packages;
|
|
1205
|
-
const file = join(root, "pnpm-workspace.yaml");
|
|
1206
|
-
let source;
|
|
1207
|
-
try {
|
|
1208
|
-
source = await readFile(file, "utf8");
|
|
1209
|
-
} catch (error) {
|
|
1210
|
-
throw new ConformanceError(`the layout has no 'packages' and ${file} cannot be read`, { cause: error });
|
|
1211
|
-
}
|
|
1212
|
-
const document = parse(source);
|
|
1213
|
-
if (typeof document !== "object" || document === null || !("packages" in document) || !Array.isArray(document.packages)) throw new ConformanceError(`${file} has no 'packages' list`);
|
|
1214
|
-
const patterns = document.packages;
|
|
1215
|
-
if (!patterns.every((pattern) => typeof pattern === "string")) throw new ConformanceError(`${file}: every entry of 'packages' must be a string`);
|
|
1216
|
-
return patterns;
|
|
1217
|
-
}
|
|
1218
|
-
async function readName(manifest) {
|
|
1219
|
-
let content;
|
|
1220
|
-
try {
|
|
1221
|
-
content = JSON.parse(await readFile(manifest, "utf8"));
|
|
1222
|
-
} catch (error) {
|
|
1223
|
-
throw new ConformanceError(`${manifest} cannot be read as JSON`, { cause: error });
|
|
1224
|
-
}
|
|
1225
|
-
if (typeof content === "object" && content !== null && "name" in content && typeof content.name === "string") return content.name;
|
|
1226
|
-
}
|
|
1227
|
-
/**
|
|
1228
|
-
* The directories under `root` that the pnpm-style `patterns` select and that hold a `package.json`, sorted. A `!` pattern excludes. The root package itself (`.`) is not a member of any group and is left out.
|
|
1229
|
-
*/
|
|
1230
|
-
async function discoverPackages(root, patterns) {
|
|
1231
|
-
const manifests = patterns.map((pattern) => pattern.startsWith("!") ? `!${pattern.slice(1)}/**` : `${pattern}/package.json`);
|
|
1232
|
-
const dirs = (await glob(manifests, {
|
|
1233
|
-
cwd: root,
|
|
1234
|
-
dot: true,
|
|
1235
|
-
ignore: [INSTALLED_DEPENDENCIES_GLOB],
|
|
1236
|
-
onlyFiles: true
|
|
1237
|
-
})).map((manifest) => manifest.replace(/\/?package\.json$/u, "")).filter((dir) => dir !== "");
|
|
1238
|
-
return Promise.all([...new Set(dirs)].sort().map(async (dir) => ({
|
|
1239
|
-
dir,
|
|
1240
|
-
name: await readName(join(root, dir, "package.json"))
|
|
1241
|
-
})));
|
|
1242
|
-
}
|
|
1243
|
-
/**
|
|
1244
|
-
* The layout's packages found under the workspace root.
|
|
1245
|
-
*/
|
|
1246
|
-
async function readWorkspacePackages(cwd, layout) {
|
|
1247
|
-
const root = workspaceRoot(cwd, layout);
|
|
1248
|
-
return classifyPackages(await discoverPackages(root, await workspacePatterns(root, layout)), layout);
|
|
1249
|
-
}
|
|
1250
|
-
//#endregion
|
|
1251
1408
|
//#region src/checks/import-graph.ts
|
|
1252
1409
|
/**
|
|
1253
1410
|
* Paths left out of the import graph unless the options say otherwise: a package's own installed dependencies.
|
|
@@ -2127,6 +2284,9 @@ function runsOnOf(reader, value, what) {
|
|
|
2127
2284
|
group
|
|
2128
2285
|
};
|
|
2129
2286
|
}
|
|
2287
|
+
function environmentOf(reader, value, what) {
|
|
2288
|
+
return isRecord(value) ? reader.text(value["name"], `${what}.name`) : reader.text(value, what);
|
|
2289
|
+
}
|
|
2130
2290
|
function stepOf(reader, value, jobPath, index) {
|
|
2131
2291
|
const path = [
|
|
2132
2292
|
...jobPath,
|
|
@@ -2159,6 +2319,7 @@ function jobOf(reader, id, value) {
|
|
|
2159
2319
|
needs: reader.list(job["needs"], `${what}.needs`),
|
|
2160
2320
|
condition: reader.text(job["if"], `${what}.if`),
|
|
2161
2321
|
permissions: permissionsOf(reader, job["permissions"], `${what}.permissions`),
|
|
2322
|
+
environment: environmentOf(reader, job["environment"], `${what}.environment`),
|
|
2162
2323
|
env: reader.texts(job["env"], `${what}.env`),
|
|
2163
2324
|
runsOn: runsOnOf(reader, job["runs-on"], `${what}.runs-on`),
|
|
2164
2325
|
timeoutMinutes: reader.text(job["timeout-minutes"], `${what}.timeout-minutes`),
|
|
@@ -2194,6 +2355,7 @@ function parseWorkflow(file, source) {
|
|
|
2194
2355
|
triggers,
|
|
2195
2356
|
triggerLocations: triggerLocationsOf(reader, triggers, root["on"]),
|
|
2196
2357
|
permissions: permissionsOf(reader, root["permissions"], "permissions"),
|
|
2358
|
+
permissionsLocation: root["permissions"] === void 0 ? void 0 : reader.location(["permissions"]),
|
|
2197
2359
|
env: reader.texts(root["env"], "env"),
|
|
2198
2360
|
jobs: Object.entries(reader.record(root["jobs"], "jobs")).map(([id, job]) => jobOf(reader, id, job)),
|
|
2199
2361
|
callOutputs: Object.fromEntries(Object.entries(outputs).map(([name, output]) => [name, reader.text(reader.record(output, `on.workflow_call.outputs.${name}`)["value"], `on.workflow_call.outputs.${name}.value`) ?? ""]))
|
|
@@ -2372,6 +2534,14 @@ function callsAlways(condition) {
|
|
|
2372
2534
|
* The default ids of junction jobs, shared by the checks that look for one.
|
|
2373
2535
|
*/
|
|
2374
2536
|
const DEFAULT_JUNCTION_JOBS = ["required-checks"];
|
|
2537
|
+
/**
|
|
2538
|
+
* The default ids of release jobs, shared by the checks that look for one.
|
|
2539
|
+
*/
|
|
2540
|
+
const DEFAULT_RELEASE_JOBS = ["release"];
|
|
2541
|
+
/**
|
|
2542
|
+
* The variables a token for the npm registry is passed in.
|
|
2543
|
+
*/
|
|
2544
|
+
const REGISTRY_TOKEN_VARIABLES = ["NODE_AUTH_TOKEN", "NPM_TOKEN"];
|
|
2375
2545
|
//#endregion
|
|
2376
2546
|
//#region src/checks/settings-required-checks.ts
|
|
2377
2547
|
/**
|
|
@@ -2522,10 +2692,6 @@ const PACKAGE_MANAGER_OPTIONS = String.raw`(?:\s+(?:(?:--filter|-F|--workspace|-
|
|
|
2522
2692
|
* Commands that publish a package to a registry.
|
|
2523
2693
|
*/
|
|
2524
2694
|
const PUBLISH_COMMAND = new RegExp(String.raw`^(?:(?:npm|pnpm|yarn)${PACKAGE_MANAGER_OPTIONS}(?:\s+npm)?\s+publish|(?:(?:pnpm|yarn)(?:\s+exec)?\s+|npx\s+)?(?:semantic-release|changeset\s+publish|lerna\s+publish))(?:\s|$)`, "u");
|
|
2525
|
-
/**
|
|
2526
|
-
* The variables a token for the registry is passed in.
|
|
2527
|
-
*/
|
|
2528
|
-
const TOKEN_VARIABLES = ["NODE_AUTH_TOKEN", "NPM_TOKEN"];
|
|
2529
2695
|
const PROVENANCE_FLAG = /--provenance(?:=true)?(?:\s|$)/u;
|
|
2530
2696
|
/**
|
|
2531
2697
|
* Whether the `package.json` in the working directory asks for provenance with `publishConfig.provenance: true`, which applies to every publish of that package.
|
|
@@ -2549,7 +2715,7 @@ function attestationViolations(workflow, job) {
|
|
|
2549
2715
|
* Whether the job passes a registry token to a step, which makes its publish not tokenless: a token variable with a value other than the empty string, anywhere in the environment.
|
|
2550
2716
|
*/
|
|
2551
2717
|
function passesToken(workflow, job) {
|
|
2552
|
-
return [void 0, ...job.steps].some((step) =>
|
|
2718
|
+
return [void 0, ...job.steps].some((step) => REGISTRY_TOKEN_VARIABLES.some((variable) => (effectiveEnv(workflow, job, step)[variable] ?? "") !== ""));
|
|
2553
2719
|
}
|
|
2554
2720
|
function tokenlessPublishViolations(workflow, job, provenanceInManifest) {
|
|
2555
2721
|
if (accessOf(effectivePermissions(workflow, job), "id-token") !== "write" || passesToken(workflow, job)) return [];
|
|
@@ -2557,9 +2723,9 @@ function tokenlessPublishViolations(workflow, job, provenanceInManifest) {
|
|
|
2557
2723
|
const publishes = scriptCommands(step.run).filter((line) => PUBLISH_COMMAND.test(line));
|
|
2558
2724
|
if (publishes.length === 0) return [];
|
|
2559
2725
|
const env = effectiveEnv(workflow, job, step);
|
|
2560
|
-
const blanked =
|
|
2726
|
+
const blanked = REGISTRY_TOKEN_VARIABLES.every((variable) => env[variable] === "");
|
|
2561
2727
|
const provenance = provenanceInManifest || publishes.some((line) => PROVENANCE_FLAG.test(line)) || env["NPM_CONFIG_PROVENANCE"] === "true";
|
|
2562
|
-
return blanked || provenance ? [] : [workflowViolation(workflow, "workflow-credentials/tokenless-publish-unprotected", `job '${job.id}' publishes with an OIDC identity and no token, but does not set ${
|
|
2728
|
+
return blanked || provenance ? [] : [workflowViolation(workflow, "workflow-credentials/tokenless-publish-unprotected", `job '${job.id}' publishes with an OIDC identity and no token, but does not set ${REGISTRY_TOKEN_VARIABLES.join(" and ")} to the empty string, so a token inherited from the environment is used when the OIDC exchange fails; set them to the empty string to prevent that, or request provenance so that such a publish at least carries a traceable attestation`, step.location)];
|
|
2563
2729
|
});
|
|
2564
2730
|
}
|
|
2565
2731
|
/**
|
|
@@ -2577,7 +2743,6 @@ const workflowCredentials = async ({ cwd, options }) => {
|
|
|
2577
2743
|
};
|
|
2578
2744
|
//#endregion
|
|
2579
2745
|
//#region src/checks/workflow-job-ordering.ts
|
|
2580
|
-
const DEFAULT_RELEASE_JOBS = ["release"];
|
|
2581
2746
|
const DEFAULT_DEPLOY_JOBS = ["deploy"];
|
|
2582
2747
|
const DEFAULT_DOCS_DEPLOY_JOBS = ["docs-deploy"];
|
|
2583
2748
|
const DEFAULT_BRANCH = "main";
|
|
@@ -2725,6 +2890,71 @@ const workflowMergeGroup = async ({ cwd, options }) => (await loadWorkflows(cwd,
|
|
|
2725
2890
|
return location === void 0 || workflow.triggers.has("merge_group") ? [] : [workflowViolation(workflow, "workflow-merge-group/missing-trigger", "the workflow runs for pull requests but has no merge_group trigger, so its checks do not report for a pull request in the merge queue", location)];
|
|
2726
2891
|
});
|
|
2727
2892
|
//#endregion
|
|
2893
|
+
//#region src/checks/workflow-release-job.ts
|
|
2894
|
+
const CHECKOUT_ACTION$1 = "actions/checkout";
|
|
2895
|
+
/**
|
|
2896
|
+
* A value that reads the `secrets` context inside an expression.
|
|
2897
|
+
*/
|
|
2898
|
+
const FROM_SECRET = /\$\{\{[^}]*\bsecrets\./u;
|
|
2899
|
+
/**
|
|
2900
|
+
* The release jobs named for a message: `release job 'a'` or `release jobs 'a' and 'b'`.
|
|
2901
|
+
*/
|
|
2902
|
+
function releaseJobsText(releases) {
|
|
2903
|
+
const ids = releases.map((job) => `'${job.id}'`);
|
|
2904
|
+
return `release job${ids.length === 1 ? "" : "s"} ${ids.join(" and ")}`;
|
|
2905
|
+
}
|
|
2906
|
+
function grantsIdToken(permissions) {
|
|
2907
|
+
return accessOf(permissions, "id-token") === "write";
|
|
2908
|
+
}
|
|
2909
|
+
function idTokenViolations(workflow, releases) {
|
|
2910
|
+
const releaseIds = new Set(releases.map((job) => job.id));
|
|
2911
|
+
const named = releaseJobsText(releases);
|
|
2912
|
+
const workflowLevel = grantsIdToken(workflow.permissions) && workflow.permissionsLocation !== void 0 ? [workflowViolation(workflow, "workflow-release-job/id-token-workflow-level", `the workflow's permissions grant id-token: write to every job that declares none of its own; grant it on ${named} only`, workflow.permissionsLocation)] : [];
|
|
2913
|
+
const jobLevel = workflow.jobs.filter((job) => !releaseIds.has(job.id) && grantsIdToken(job.permissions)).map((job) => workflowViolation(workflow, "workflow-release-job/id-token-outside-release", `job '${job.id}' is granted id-token: write but is not a release job, so it can mint the OIDC identity npm trusted publishing accepts for this workflow; only ${named} may hold it`, job.location));
|
|
2914
|
+
return [...workflowLevel, ...jobLevel];
|
|
2915
|
+
}
|
|
2916
|
+
/**
|
|
2917
|
+
* What a checkout that does not set `persist-credentials: false` leaves in git config.
|
|
2918
|
+
*/
|
|
2919
|
+
function persistedCredential(step) {
|
|
2920
|
+
if (step.with["ssh-key"] !== void 0) return "the SSH key";
|
|
2921
|
+
return step.with["token"] === void 0 ? "the job token" : "the token it was given";
|
|
2922
|
+
}
|
|
2923
|
+
function releaseJobViolations(workflow, job) {
|
|
2924
|
+
if (job.uses !== void 0) return [];
|
|
2925
|
+
const environment = job.environment === void 0 ? [workflowViolation(workflow, "workflow-release-job/missing-environment", `release job '${job.id}' declares no environment, so nothing outside the workflow file stops a branch's copy of it publishing; run it in a protected environment restricted to the default branch`, job.location)] : [];
|
|
2926
|
+
const checkouts = job.steps.filter((step) => stepUses(step, CHECKOUT_ACTION$1) && step.with["persist-credentials"] !== "false").map((step) => workflowViolation(workflow, "workflow-release-job/persisted-credentials", `release job '${job.id}' checks out without persist-credentials: false, which leaves ${persistedCredential(step)} in git config for every later step`, step.location));
|
|
2927
|
+
return [...environment, ...checkouts];
|
|
2928
|
+
}
|
|
2929
|
+
/**
|
|
2930
|
+
* The token variables an environment sets from a secret.
|
|
2931
|
+
*/
|
|
2932
|
+
function secretTokens(env) {
|
|
2933
|
+
return REGISTRY_TOKEN_VARIABLES.filter((variable) => FROM_SECRET.test(env[variable] ?? ""));
|
|
2934
|
+
}
|
|
2935
|
+
function tokenBesideOidcViolations(workflow, job) {
|
|
2936
|
+
if (!grantsIdToken(effectivePermissions(workflow, job))) return [];
|
|
2937
|
+
const report = (variables, location) => variables.length === 0 ? [] : [workflowViolation(workflow, "workflow-release-job/token-beside-oidc", `job '${job.id}' holds id-token: write and passes ${variables.join(" and ")} from a secret, a long-lived npm token beside the OIDC identity; publish through trusted publishing and blank the token variables`, location)];
|
|
2938
|
+
return [...report(secretTokens(effectiveEnv(workflow, job, void 0)), job.location), ...job.steps.flatMap((step) => report(secretTokens(step.env), step.location))];
|
|
2939
|
+
}
|
|
2940
|
+
/**
|
|
2941
|
+
* npm binds a trusted publisher to the repository and the workflow file, not to a branch, so anyone who can push a branch can edit that branch's copy of the workflow, drop an `if` that keeps publishing to the default branch, and publish. What stops that has to live outside the file: a protected environment that only the default branch may deploy to, named in npm's trusted publisher too. This check holds the release job to the shape that makes that protection hold.
|
|
2942
|
+
*
|
|
2943
|
+
* In a workflow with a release job (by id, `releaseJobs`, `release` by default): `workflow-release-job/id-token-workflow-level`, the workflow's own `permissions` grant `id-token: write` (directly or through `write-all`), which every job that declares none inherits; `id-token-outside-release`, a job that is not a release job declares `id-token: write` itself, a job that calls a reusable workflow included; `missing-environment`, a release job declares no `environment`; `persisted-credentials`, a release job runs `actions/checkout` without `persist-credentials: false` (literally; an expression is unknown and reported), so the job token, a `token` input or an `ssh-key` stays in git config for every later step. A release job that pushes as a deploy key provisions the key in a step and hands it to git through that step's `GIT_SSH_COMMAND`, after a checkout that persists nothing, and is accepted as written; semantic-release authenticates its own push (`repositoryUrl` as written, else the URL with `GITHUB_TOKEN` or `GH_TOKEN` from the environment), so it needs nothing from the checkout.
|
|
2944
|
+
*
|
|
2945
|
+
* In every workflow: `token-beside-oidc`, a job whose effective permissions grant `id-token: write` sets `NODE_AUTH_TOKEN` or `NPM_TOKEN` from the `secrets` context in its workflow, job or step `env`.
|
|
2946
|
+
*
|
|
2947
|
+
* Limits: the environment's protection rules and npm's trusted publisher settings are not visible in the file and are not read; a release job that calls a reusable workflow is judged only for `id-token`, since `environment` and steps belong to the called workflow; a token passed through `with`, written to an `.npmrc` by a script, or inherited through a composite action is not seen.
|
|
2948
|
+
*/
|
|
2949
|
+
const workflowReleaseJob = async ({ cwd, options }) => {
|
|
2950
|
+
return (await loadWorkflows(cwd, options)).flatMap((workflow) => {
|
|
2951
|
+
const releases = jobsNamed(workflow, options.releaseJobs ?? DEFAULT_RELEASE_JOBS);
|
|
2952
|
+
const releaseFindings = releases.length === 0 ? [] : idTokenViolations(workflow, releases);
|
|
2953
|
+
const jobFindings = workflow.jobs.flatMap((job) => [...releases.includes(job) ? releaseJobViolations(workflow, job) : [], ...tokenBesideOidcViolations(workflow, job)]);
|
|
2954
|
+
return [...releaseFindings, ...jobFindings];
|
|
2955
|
+
});
|
|
2956
|
+
};
|
|
2957
|
+
//#endregion
|
|
2728
2958
|
//#region src/checks/workflow-repository-dispatch.ts
|
|
2729
2959
|
const DEFAULT_BRANCHES = ["main", "master"];
|
|
2730
2960
|
const DEFAULT_BRANCH_EXPRESSION = /\$\{\{\s*github\.event\.repository\.default_branch\s*\}\}/u;
|
|
@@ -3257,6 +3487,7 @@ const checks = z.strictObject({
|
|
|
3257
3487
|
commitlint: z.exactOptional(path),
|
|
3258
3488
|
release: z.exactOptional(path)
|
|
3259
3489
|
}))),
|
|
3490
|
+
"engines-floor": z.exactOptional(enabled(z.strictObject({ packages: z.exactOptional(paths) }))),
|
|
3260
3491
|
"dockerfile-package-manager": z.exactOptional(enabled(z.strictObject({
|
|
3261
3492
|
dockerfiles: z.exactOptional(paths),
|
|
3262
3493
|
exclude: z.exactOptional(paths),
|
|
@@ -3301,6 +3532,10 @@ const checks = z.strictObject({
|
|
|
3301
3532
|
}))),
|
|
3302
3533
|
"workflow-version-single-source": z.exactOptional(enabled(z.strictObject(selection))),
|
|
3303
3534
|
"workflow-credentials": z.exactOptional(enabled(z.strictObject(selection))),
|
|
3535
|
+
"workflow-release-job": z.exactOptional(enabled(z.strictObject({
|
|
3536
|
+
...selection,
|
|
3537
|
+
releaseJobs: z.exactOptional(names)
|
|
3538
|
+
}))),
|
|
3304
3539
|
"workflow-repository-dispatch": z.exactOptional(enabled(z.strictObject({
|
|
3305
3540
|
...selection,
|
|
3306
3541
|
defaultBranches: z.exactOptional(names),
|
|
@@ -3546,6 +3781,12 @@ const registry = {
|
|
|
3546
3781
|
select: (checks) => enabledOptions(checks["commit-types"]),
|
|
3547
3782
|
check: commitTypes
|
|
3548
3783
|
}),
|
|
3784
|
+
"engines-floor": plain({
|
|
3785
|
+
name: "engines-floor",
|
|
3786
|
+
description: "A package's engines.node range admits no Node version that one of its installed dependencies rejects",
|
|
3787
|
+
select: (checks) => enabledOptions(checks["engines-floor"]),
|
|
3788
|
+
check: enginesFloor
|
|
3789
|
+
}),
|
|
3549
3790
|
"dockerfile-package-manager": plain({
|
|
3550
3791
|
name: "dockerfile-package-manager",
|
|
3551
3792
|
description: "A package manager version pinned in a Dockerfile is the version packageManager names",
|
|
@@ -3594,6 +3835,12 @@ const registry = {
|
|
|
3594
3835
|
select: (checks) => enabledOptions(checks["workflow-credentials"]),
|
|
3595
3836
|
check: workflowCredentials
|
|
3596
3837
|
}),
|
|
3838
|
+
"workflow-release-job": plain({
|
|
3839
|
+
name: "workflow-release-job",
|
|
3840
|
+
description: "The release job alone holds id-token, runs in an environment and persists no credentials, and no job with id-token passes an npm token secret",
|
|
3841
|
+
select: (checks) => enabledOptions(checks["workflow-release-job"]),
|
|
3842
|
+
check: workflowReleaseJob
|
|
3843
|
+
}),
|
|
3597
3844
|
"workflow-repository-dispatch": plain({
|
|
3598
3845
|
name: "workflow-repository-dispatch",
|
|
3599
3846
|
description: "A repository_dispatch handler does not push to the default branch, auto-merge unguarded, or rely on the default token",
|
|
@@ -3845,4 +4092,4 @@ function proveExhaustive(input) {
|
|
|
3845
4092
|
}) };
|
|
3846
4093
|
}
|
|
3847
4094
|
//#endregion
|
|
3848
|
-
export { ConformanceError, DEFAULT_DERIVED_TYPE_INFERENCES, DEFAULT_INFERENCES, DEFAULT_WORKFLOWS, EXIT_CODES, ExhaustivenessError, PRESET_COMMIT_TYPES, accessOf, aggregateMappers, checkNames, classifyPackages, codecPairs, commandTypes, commitTypes, conformanceSchema, conformanceSection, createGitHubClient, crossSliceRules, cycleRule, deployTools, derivedTypes, dockerfilePackageManager, effectiveEnv, effectivePermissions, eslint, generators, handlerCases, importCrossSlice, importCycles, importIsolatedGroups, importRankSkip, importUphill, instructionSymlinks, isCheckName, isolatedGroupRules, loadWorkflows, migrationsDirectory, packagesPattern, parseUses, parseWorkflow, proveExhaustive, rankSkipRules, registry, removeCase, runChecks, settingsMergeMethods, settingsRequiredChecks, settingsReviewThreadResolution, singleStorybook, transitiveNeeds, typeErrors, uphillRules, workflowActionPinning, workflowCredentials, workflowJobOrdering, workflowMergeGroup, workflowRepositoryDispatch, workflowRunnerResolution, workflowSkippableJobs, workflowUpdateBotCooldown, workflowVersionSingleSource };
|
|
4095
|
+
export { ConformanceError, DEFAULT_DERIVED_TYPE_INFERENCES, DEFAULT_INFERENCES, DEFAULT_WORKFLOWS, EXIT_CODES, ExhaustivenessError, PRESET_COMMIT_TYPES, accessOf, aggregateMappers, checkNames, classifyPackages, codecPairs, commandTypes, commitTypes, conformanceSchema, conformanceSection, createGitHubClient, crossSliceRules, cycleRule, deployTools, derivedTypes, dockerfilePackageManager, effectiveEnv, effectivePermissions, enginesFloor, eslint, generators, handlerCases, importCrossSlice, importCycles, importIsolatedGroups, importRankSkip, importUphill, instructionSymlinks, isCheckName, isolatedGroupRules, loadWorkflows, migrationsDirectory, packagesPattern, parseUses, parseWorkflow, proveExhaustive, rankSkipRules, registry, removeCase, runChecks, settingsMergeMethods, settingsRequiredChecks, settingsReviewThreadResolution, singleStorybook, transitiveNeeds, typeErrors, uphillRules, workflowActionPinning, workflowCredentials, workflowJobOrdering, workflowMergeGroup, workflowReleaseJob, workflowRepositoryDispatch, workflowRunnerResolution, workflowSkippableJobs, workflowUpdateBotCooldown, workflowVersionSingleSource };
|