workspace-conformance 1.4.0 → 1.6.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 +18 -1
- package/dist/cli.js +219 -8
- package/dist/index.cjs +221 -8
- package/dist/index.d.cts +65 -1
- package/dist/index.d.ts +65 -1
- package/dist/index.js +220 -9
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -99,6 +99,23 @@ function expandTemplate(template, values) {
|
|
|
99
99
|
return value;
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
|
+
const NAME_PLACEHOLDER = "{name}";
|
|
103
|
+
/**
|
|
104
|
+
* What a name template must be, as a configuration error says it. It does not quote the template, as no configuration error quotes a value.
|
|
105
|
+
*/
|
|
106
|
+
const NAME_TEMPLATE_REQUIREMENT = "needs {name} exactly once, other text beside it and no other placeholder";
|
|
107
|
+
const ANY_PLACEHOLDER = /\{[a-z]+\}/u;
|
|
108
|
+
/**
|
|
109
|
+
* `template` split at `{name}`, or `undefined` unless it holds `{name}` exactly once, other text beside it and no other placeholder. Without other text every name would match, and with `{name}` twice a name could be split more than one way.
|
|
110
|
+
*/
|
|
111
|
+
function parseNameTemplate(template) {
|
|
112
|
+
const [before, after, ...rest] = template.split(NAME_PLACEHOLDER);
|
|
113
|
+
if (before === void 0 || after === void 0 || rest.length > 0 || before + after === "" || ANY_PLACEHOLDER.test(before + after)) return;
|
|
114
|
+
return {
|
|
115
|
+
before,
|
|
116
|
+
after
|
|
117
|
+
};
|
|
118
|
+
}
|
|
102
119
|
//#endregion
|
|
103
120
|
//#region src/ts-project.ts
|
|
104
121
|
/**
|
|
@@ -212,6 +229,97 @@ const aggregateMappers = async ({ cwd, options }) => {
|
|
|
212
229
|
return violations.sort((a, b) => a.file.localeCompare(b.file) || (a.location?.line ?? 0) - (b.location?.line ?? 0) || a.message.localeCompare(b.message));
|
|
213
230
|
};
|
|
214
231
|
//#endregion
|
|
232
|
+
//#region src/checks/codec-pairs.ts
|
|
233
|
+
function collect(container, prefix, found, entered) {
|
|
234
|
+
for (const [name, declarations] of container.getExportedDeclarations()) {
|
|
235
|
+
const value = declarations.find((declaration) => !Node.isInterfaceDeclaration(declaration) && !Node.isTypeAliasDeclaration(declaration) && !Node.isModuleDeclaration(declaration) && !Node.isSourceFile(declaration));
|
|
236
|
+
if (value !== void 0) found.set(`${prefix}${name}`, value);
|
|
237
|
+
for (const member of declarations) if (Node.isModuleDeclaration(member)) collect(member, `${prefix}${name}.`, found, entered);
|
|
238
|
+
else if (Node.isSourceFile(member) && !entered.has(member)) collect(member, `${prefix}${name}.`, found, /* @__PURE__ */ new Set([...entered, member]));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* The values `file` exports, by name, wherever they are declared: a re-export counts. Those inside an exported namespace, or in a module re-exported as a namespace (`export * as ns from './x'`), are included under their qualified name (`Namespace.value`). Interfaces and type aliases are types, not codecs, and a namespace is where codecs live rather than one itself, so all three are left out.
|
|
243
|
+
*/
|
|
244
|
+
function exportedValues(file) {
|
|
245
|
+
const found = /* @__PURE__ */ new Map();
|
|
246
|
+
collect(file, "", found, /* @__PURE__ */ new Set([file]));
|
|
247
|
+
return found;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Where the declaration's name is written, or where the declaration starts when it has no name node.
|
|
251
|
+
*/
|
|
252
|
+
function valueLocation(declaration) {
|
|
253
|
+
const nameNode = Node.hasName(declaration) ? declaration.getNameNode() : void 0;
|
|
254
|
+
const { line, column } = declaration.getSourceFile().getLineAndColumnAtPos((nameNode ?? declaration).getStart());
|
|
255
|
+
return {
|
|
256
|
+
line,
|
|
257
|
+
column
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The template of an option, which the configuration has already validated; a template that does not parse is a configuration error naming the option.
|
|
262
|
+
*/
|
|
263
|
+
function templateOf(options, option) {
|
|
264
|
+
const template = parseNameTemplate(options[option]);
|
|
265
|
+
if (template === void 0) throw new ConformanceError(`checks.codec-pairs.${option}: ${NAME_TEMPLATE_REQUIREMENT}`);
|
|
266
|
+
return template;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* The shared name a qualified export name gives under `template`, or `undefined` when its last segment does not fit the template. The namespace part is kept apart so a counterpart is looked for in the same namespace.
|
|
270
|
+
*/
|
|
271
|
+
function sharedName(qualified, template) {
|
|
272
|
+
const namespace = qualified.slice(0, qualified.lastIndexOf(".") + 1);
|
|
273
|
+
const local = qualified.slice(namespace.length);
|
|
274
|
+
if (local.length <= template.before.length + template.after.length || !local.startsWith(template.before) || !local.endsWith(template.after)) return;
|
|
275
|
+
return {
|
|
276
|
+
namespace,
|
|
277
|
+
name: local.slice(template.before.length, local.length - template.after.length)
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Every encoder a codec file exports has the decoder its name implies, exported by the same file, and every decoder the encoder. A name is an encoder or a decoder when it fits that template; other exports are not codecs and are not judged. A half re-exported from another file counts, and is reported where it is declared.
|
|
282
|
+
*
|
|
283
|
+
* It checks that the counterpart is exported, not that the two round-trip: that is a property test of the repository's own.
|
|
284
|
+
*/
|
|
285
|
+
const codecPairs = async ({ cwd, options }) => {
|
|
286
|
+
const { project, files } = await createProject(cwd, options.tsConfig, options.codecs, {
|
|
287
|
+
check: "codec-pairs",
|
|
288
|
+
files: "codecs"
|
|
289
|
+
});
|
|
290
|
+
const excluded = new Set(options.exclude);
|
|
291
|
+
const encoder = templateOf(options, "encoder");
|
|
292
|
+
const decoder = templateOf(options, "decoder");
|
|
293
|
+
const halves = [{
|
|
294
|
+
template: encoder,
|
|
295
|
+
counterpart: decoder,
|
|
296
|
+
missing: "decoder"
|
|
297
|
+
}, {
|
|
298
|
+
template: decoder,
|
|
299
|
+
counterpart: encoder,
|
|
300
|
+
missing: "encoder"
|
|
301
|
+
}];
|
|
302
|
+
const violations = [];
|
|
303
|
+
for (const file of files) {
|
|
304
|
+
const values = exportedValues(project.getSourceFileOrThrow(resolve(cwd, file)));
|
|
305
|
+
for (const [qualified, declaration] of values) {
|
|
306
|
+
if (excluded.has(qualified)) continue;
|
|
307
|
+
for (const { template, counterpart, missing } of halves) {
|
|
308
|
+
const shared = sharedName(qualified, template);
|
|
309
|
+
if (shared === void 0) continue;
|
|
310
|
+
const expected = `${shared.namespace}${counterpart.before}${shared.name}${counterpart.after}`;
|
|
311
|
+
if (!values.has(expected)) violations.push({
|
|
312
|
+
code: `codec-pairs/missing-${missing}`,
|
|
313
|
+
message: `${qualified} has no ${missing}: ${file} does not export ${expected}`,
|
|
314
|
+
file: relativePosix(cwd, declaration.getSourceFile().getFilePath()),
|
|
315
|
+
location: valueLocation(declaration)
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return violations.sort((a, b) => a.file.localeCompare(b.file) || (a.location?.line ?? 0) - (b.location?.line ?? 0) || a.message.localeCompare(b.message));
|
|
321
|
+
};
|
|
322
|
+
//#endregion
|
|
215
323
|
//#region src/checks/type-references.ts
|
|
216
324
|
/**
|
|
217
325
|
* The identifier a reference ends in: `infer` in `z.infer`, the name itself when it is not qualified.
|
|
@@ -2019,6 +2127,9 @@ function runsOnOf(reader, value, what) {
|
|
|
2019
2127
|
group
|
|
2020
2128
|
};
|
|
2021
2129
|
}
|
|
2130
|
+
function environmentOf(reader, value, what) {
|
|
2131
|
+
return isRecord(value) ? reader.text(value["name"], `${what}.name`) : reader.text(value, what);
|
|
2132
|
+
}
|
|
2022
2133
|
function stepOf(reader, value, jobPath, index) {
|
|
2023
2134
|
const path = [
|
|
2024
2135
|
...jobPath,
|
|
@@ -2051,6 +2162,7 @@ function jobOf(reader, id, value) {
|
|
|
2051
2162
|
needs: reader.list(job["needs"], `${what}.needs`),
|
|
2052
2163
|
condition: reader.text(job["if"], `${what}.if`),
|
|
2053
2164
|
permissions: permissionsOf(reader, job["permissions"], `${what}.permissions`),
|
|
2165
|
+
environment: environmentOf(reader, job["environment"], `${what}.environment`),
|
|
2054
2166
|
env: reader.texts(job["env"], `${what}.env`),
|
|
2055
2167
|
runsOn: runsOnOf(reader, job["runs-on"], `${what}.runs-on`),
|
|
2056
2168
|
timeoutMinutes: reader.text(job["timeout-minutes"], `${what}.timeout-minutes`),
|
|
@@ -2086,6 +2198,7 @@ function parseWorkflow(file, source) {
|
|
|
2086
2198
|
triggers,
|
|
2087
2199
|
triggerLocations: triggerLocationsOf(reader, triggers, root["on"]),
|
|
2088
2200
|
permissions: permissionsOf(reader, root["permissions"], "permissions"),
|
|
2201
|
+
permissionsLocation: root["permissions"] === void 0 ? void 0 : reader.location(["permissions"]),
|
|
2089
2202
|
env: reader.texts(root["env"], "env"),
|
|
2090
2203
|
jobs: Object.entries(reader.record(root["jobs"], "jobs")).map(([id, job]) => jobOf(reader, id, job)),
|
|
2091
2204
|
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`) ?? ""]))
|
|
@@ -2264,6 +2377,14 @@ function callsAlways(condition) {
|
|
|
2264
2377
|
* The default ids of junction jobs, shared by the checks that look for one.
|
|
2265
2378
|
*/
|
|
2266
2379
|
const DEFAULT_JUNCTION_JOBS = ["required-checks"];
|
|
2380
|
+
/**
|
|
2381
|
+
* The default ids of release jobs, shared by the checks that look for one.
|
|
2382
|
+
*/
|
|
2383
|
+
const DEFAULT_RELEASE_JOBS = ["release"];
|
|
2384
|
+
/**
|
|
2385
|
+
* The variables a token for the npm registry is passed in.
|
|
2386
|
+
*/
|
|
2387
|
+
const REGISTRY_TOKEN_VARIABLES = ["NODE_AUTH_TOKEN", "NPM_TOKEN"];
|
|
2267
2388
|
//#endregion
|
|
2268
2389
|
//#region src/checks/settings-required-checks.ts
|
|
2269
2390
|
/**
|
|
@@ -2414,10 +2535,6 @@ const PACKAGE_MANAGER_OPTIONS = String.raw`(?:\s+(?:(?:--filter|-F|--workspace|-
|
|
|
2414
2535
|
* Commands that publish a package to a registry.
|
|
2415
2536
|
*/
|
|
2416
2537
|
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");
|
|
2417
|
-
/**
|
|
2418
|
-
* The variables a token for the registry is passed in.
|
|
2419
|
-
*/
|
|
2420
|
-
const TOKEN_VARIABLES = ["NODE_AUTH_TOKEN", "NPM_TOKEN"];
|
|
2421
2538
|
const PROVENANCE_FLAG = /--provenance(?:=true)?(?:\s|$)/u;
|
|
2422
2539
|
/**
|
|
2423
2540
|
* Whether the `package.json` in the working directory asks for provenance with `publishConfig.provenance: true`, which applies to every publish of that package.
|
|
@@ -2441,7 +2558,7 @@ function attestationViolations(workflow, job) {
|
|
|
2441
2558
|
* 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.
|
|
2442
2559
|
*/
|
|
2443
2560
|
function passesToken(workflow, job) {
|
|
2444
|
-
return [void 0, ...job.steps].some((step) =>
|
|
2561
|
+
return [void 0, ...job.steps].some((step) => REGISTRY_TOKEN_VARIABLES.some((variable) => (effectiveEnv(workflow, job, step)[variable] ?? "") !== ""));
|
|
2445
2562
|
}
|
|
2446
2563
|
function tokenlessPublishViolations(workflow, job, provenanceInManifest) {
|
|
2447
2564
|
if (accessOf(effectivePermissions(workflow, job), "id-token") !== "write" || passesToken(workflow, job)) return [];
|
|
@@ -2449,9 +2566,9 @@ function tokenlessPublishViolations(workflow, job, provenanceInManifest) {
|
|
|
2449
2566
|
const publishes = scriptCommands(step.run).filter((line) => PUBLISH_COMMAND.test(line));
|
|
2450
2567
|
if (publishes.length === 0) return [];
|
|
2451
2568
|
const env = effectiveEnv(workflow, job, step);
|
|
2452
|
-
const blanked =
|
|
2569
|
+
const blanked = REGISTRY_TOKEN_VARIABLES.every((variable) => env[variable] === "");
|
|
2453
2570
|
const provenance = provenanceInManifest || publishes.some((line) => PROVENANCE_FLAG.test(line)) || env["NPM_CONFIG_PROVENANCE"] === "true";
|
|
2454
|
-
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 ${
|
|
2571
|
+
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)];
|
|
2455
2572
|
});
|
|
2456
2573
|
}
|
|
2457
2574
|
/**
|
|
@@ -2469,7 +2586,6 @@ const workflowCredentials = async ({ cwd, options }) => {
|
|
|
2469
2586
|
};
|
|
2470
2587
|
//#endregion
|
|
2471
2588
|
//#region src/checks/workflow-job-ordering.ts
|
|
2472
|
-
const DEFAULT_RELEASE_JOBS = ["release"];
|
|
2473
2589
|
const DEFAULT_DEPLOY_JOBS = ["deploy"];
|
|
2474
2590
|
const DEFAULT_DOCS_DEPLOY_JOBS = ["docs-deploy"];
|
|
2475
2591
|
const DEFAULT_BRANCH = "main";
|
|
@@ -2617,6 +2733,71 @@ const workflowMergeGroup = async ({ cwd, options }) => (await loadWorkflows(cwd,
|
|
|
2617
2733
|
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)];
|
|
2618
2734
|
});
|
|
2619
2735
|
//#endregion
|
|
2736
|
+
//#region src/checks/workflow-release-job.ts
|
|
2737
|
+
const CHECKOUT_ACTION$1 = "actions/checkout";
|
|
2738
|
+
/**
|
|
2739
|
+
* A value that reads the `secrets` context inside an expression.
|
|
2740
|
+
*/
|
|
2741
|
+
const FROM_SECRET = /\$\{\{[^}]*\bsecrets\./u;
|
|
2742
|
+
/**
|
|
2743
|
+
* The release jobs named for a message: `release job 'a'` or `release jobs 'a' and 'b'`.
|
|
2744
|
+
*/
|
|
2745
|
+
function releaseJobsText(releases) {
|
|
2746
|
+
const ids = releases.map((job) => `'${job.id}'`);
|
|
2747
|
+
return `release job${ids.length === 1 ? "" : "s"} ${ids.join(" and ")}`;
|
|
2748
|
+
}
|
|
2749
|
+
function grantsIdToken(permissions) {
|
|
2750
|
+
return accessOf(permissions, "id-token") === "write";
|
|
2751
|
+
}
|
|
2752
|
+
function idTokenViolations(workflow, releases) {
|
|
2753
|
+
const releaseIds = new Set(releases.map((job) => job.id));
|
|
2754
|
+
const named = releaseJobsText(releases);
|
|
2755
|
+
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)] : [];
|
|
2756
|
+
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));
|
|
2757
|
+
return [...workflowLevel, ...jobLevel];
|
|
2758
|
+
}
|
|
2759
|
+
/**
|
|
2760
|
+
* What a checkout that does not set `persist-credentials: false` leaves in git config.
|
|
2761
|
+
*/
|
|
2762
|
+
function persistedCredential(step) {
|
|
2763
|
+
if (step.with["ssh-key"] !== void 0) return "the SSH key";
|
|
2764
|
+
return step.with["token"] === void 0 ? "the job token" : "the token it was given";
|
|
2765
|
+
}
|
|
2766
|
+
function releaseJobViolations(workflow, job) {
|
|
2767
|
+
if (job.uses !== void 0) return [];
|
|
2768
|
+
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)] : [];
|
|
2769
|
+
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));
|
|
2770
|
+
return [...environment, ...checkouts];
|
|
2771
|
+
}
|
|
2772
|
+
/**
|
|
2773
|
+
* The token variables an environment sets from a secret.
|
|
2774
|
+
*/
|
|
2775
|
+
function secretTokens(env) {
|
|
2776
|
+
return REGISTRY_TOKEN_VARIABLES.filter((variable) => FROM_SECRET.test(env[variable] ?? ""));
|
|
2777
|
+
}
|
|
2778
|
+
function tokenBesideOidcViolations(workflow, job) {
|
|
2779
|
+
if (!grantsIdToken(effectivePermissions(workflow, job))) return [];
|
|
2780
|
+
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)];
|
|
2781
|
+
return [...report(secretTokens(effectiveEnv(workflow, job, void 0)), job.location), ...job.steps.flatMap((step) => report(secretTokens(step.env), step.location))];
|
|
2782
|
+
}
|
|
2783
|
+
/**
|
|
2784
|
+
* 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.
|
|
2785
|
+
*
|
|
2786
|
+
* 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.
|
|
2787
|
+
*
|
|
2788
|
+
* 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`.
|
|
2789
|
+
*
|
|
2790
|
+
* 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.
|
|
2791
|
+
*/
|
|
2792
|
+
const workflowReleaseJob = async ({ cwd, options }) => {
|
|
2793
|
+
return (await loadWorkflows(cwd, options)).flatMap((workflow) => {
|
|
2794
|
+
const releases = jobsNamed(workflow, options.releaseJobs ?? DEFAULT_RELEASE_JOBS);
|
|
2795
|
+
const releaseFindings = releases.length === 0 ? [] : idTokenViolations(workflow, releases);
|
|
2796
|
+
const jobFindings = workflow.jobs.flatMap((job) => [...releases.includes(job) ? releaseJobViolations(workflow, job) : [], ...tokenBesideOidcViolations(workflow, job)]);
|
|
2797
|
+
return [...releaseFindings, ...jobFindings];
|
|
2798
|
+
});
|
|
2799
|
+
};
|
|
2800
|
+
//#endregion
|
|
2620
2801
|
//#region src/checks/workflow-repository-dispatch.ts
|
|
2621
2802
|
const DEFAULT_BRANCHES = ["main", "master"];
|
|
2622
2803
|
const DEFAULT_BRANCH_EXPRESSION = /\$\{\{\s*github\.event\.repository\.default_branch\s*\}\}/u;
|
|
@@ -3070,6 +3251,10 @@ function compiles(source) {
|
|
|
3070
3251
|
*/
|
|
3071
3252
|
const pattern = z.string().min(1).refine(compiles, "not a valid regular expression");
|
|
3072
3253
|
const patterns = z.array(pattern);
|
|
3254
|
+
/**
|
|
3255
|
+
* A name template of `codec-pairs`.
|
|
3256
|
+
*/
|
|
3257
|
+
const nameTemplate = z.string().refine((template) => parseNameTemplate(template) !== void 0, NAME_TEMPLATE_REQUIREMENT);
|
|
3073
3258
|
const severity = z.enum(["warn", "error"]);
|
|
3074
3259
|
const requiredRules = z.record(z.string().min(1), severity);
|
|
3075
3260
|
const selection = {
|
|
@@ -3102,6 +3287,16 @@ const checks = z.strictObject({
|
|
|
3102
3287
|
exclude: z.exactOptional(names),
|
|
3103
3288
|
tsConfig: z.exactOptional(path)
|
|
3104
3289
|
}))),
|
|
3290
|
+
"codec-pairs": z.exactOptional(enabled(z.strictObject({
|
|
3291
|
+
codecs: paths,
|
|
3292
|
+
encoder: nameTemplate,
|
|
3293
|
+
decoder: nameTemplate,
|
|
3294
|
+
exclude: z.exactOptional(names),
|
|
3295
|
+
tsConfig: z.exactOptional(path)
|
|
3296
|
+
}).refine((options) => options.encoder !== options.decoder, {
|
|
3297
|
+
path: ["decoder"],
|
|
3298
|
+
message: "the decoder template is the encoder template, so every name would be its own pair"
|
|
3299
|
+
}))),
|
|
3105
3300
|
"command-types": z.exactOptional(enabled(z.strictObject({
|
|
3106
3301
|
commands: paths,
|
|
3107
3302
|
exclude: z.exactOptional(names),
|
|
@@ -3179,6 +3374,10 @@ const checks = z.strictObject({
|
|
|
3179
3374
|
}))),
|
|
3180
3375
|
"workflow-version-single-source": z.exactOptional(enabled(z.strictObject(selection))),
|
|
3181
3376
|
"workflow-credentials": z.exactOptional(enabled(z.strictObject(selection))),
|
|
3377
|
+
"workflow-release-job": z.exactOptional(enabled(z.strictObject({
|
|
3378
|
+
...selection,
|
|
3379
|
+
releaseJobs: z.exactOptional(names)
|
|
3380
|
+
}))),
|
|
3182
3381
|
"workflow-repository-dispatch": z.exactOptional(enabled(z.strictObject({
|
|
3183
3382
|
...selection,
|
|
3184
3383
|
defaultBranches: z.exactOptional(names),
|
|
@@ -3358,6 +3557,12 @@ const registry = {
|
|
|
3358
3557
|
select: (checks) => enabledOptions(checks["aggregate-mappers"]),
|
|
3359
3558
|
check: aggregateMappers
|
|
3360
3559
|
}),
|
|
3560
|
+
"codec-pairs": plain({
|
|
3561
|
+
name: "codec-pairs",
|
|
3562
|
+
description: "Every exported encoder in a codec file has a decoder under the naming templates, and every decoder an encoder",
|
|
3563
|
+
select: (checks) => enabledOptions(checks["codec-pairs"]),
|
|
3564
|
+
check: codecPairs
|
|
3565
|
+
}),
|
|
3361
3566
|
"command-types": plain({
|
|
3362
3567
|
name: "command-types",
|
|
3363
3568
|
description: "Every exported command type is derived from a schema, not hand-written",
|
|
@@ -3466,6 +3671,12 @@ const registry = {
|
|
|
3466
3671
|
select: (checks) => enabledOptions(checks["workflow-credentials"]),
|
|
3467
3672
|
check: workflowCredentials
|
|
3468
3673
|
}),
|
|
3674
|
+
"workflow-release-job": plain({
|
|
3675
|
+
name: "workflow-release-job",
|
|
3676
|
+
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",
|
|
3677
|
+
select: (checks) => enabledOptions(checks["workflow-release-job"]),
|
|
3678
|
+
check: workflowReleaseJob
|
|
3679
|
+
}),
|
|
3469
3680
|
"workflow-repository-dispatch": plain({
|
|
3470
3681
|
name: "workflow-repository-dispatch",
|
|
3471
3682
|
description: "A repository_dispatch handler does not push to the default branch, auto-merge unguarded, or rely on the default token",
|
|
@@ -3717,4 +3928,4 @@ function proveExhaustive(input) {
|
|
|
3717
3928
|
}) };
|
|
3718
3929
|
}
|
|
3719
3930
|
//#endregion
|
|
3720
|
-
export { ConformanceError, DEFAULT_DERIVED_TYPE_INFERENCES, DEFAULT_INFERENCES, DEFAULT_WORKFLOWS, EXIT_CODES, ExhaustivenessError, PRESET_COMMIT_TYPES, accessOf, aggregateMappers, checkNames, classifyPackages, 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 };
|
|
3931
|
+
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, workflowReleaseJob, workflowRepositoryDispatch, workflowRunnerResolution, workflowSkippableJobs, workflowUpdateBotCooldown, workflowVersionSingleSource };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workspace-conformance",
|
|
3
3
|
"description": "Conformance checks for a pnpm workspace that ESLint cannot express: type-graph, import-graph and cross-file checks driven by the shared layout section of @exadev/config",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.6.0",
|
|
5
5
|
"bin": {
|
|
6
6
|
"workspace-conformance": "./dist/cli.js"
|
|
7
7
|
},
|