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 CHANGED
@@ -159,6 +159,8 @@ Code: `derived-types/not-derived`, reported at the type's declaration. Limits: a
159
159
 
160
160
  **`commit-types`** verifies that the commit types commitlint accepts are accounted for in the semantic-release config. Options: `commitlint` (`commitlint.config.ts`) and `release` (`release.config.ts`). Both files are evaluated through the shared jiti loader, so a list derived from one shared constant compares as its values. The `alias` and `fsCache` of `runChecks`' `configFiles` apply to them as to the sections; `trust` and `merge` are for `extends` and do not. A commit type that no conventional preset knows (`PRESET_COMMIT_TYPES` lists the ones that do) must have a `releaseRules` entry in the `@semantic-release/commit-analyzer` options and, when `presetConfig.types` of `@semantic-release/release-notes-generator` is set, a changelog section. Every `releaseRules` type and every changelog type must be a commit type, or it can never apply. A preset type may be left out of either list, which is how a release rule list normally omits the types that do not release. Codes: `commit-types/no-release-rule`, `release-rule-not-a-commit-type`, `no-changelog-section`, `changelog-section-not-a-commit-type`, `no-type-enum` (a type no preset knows is listed but commitlint sets no `type-enum`, so the commit types cannot be read) and `missing-config`. Without `presetConfig.types` (or without a release notes generator) no changelog sections are listed, so none is required. Limits: `type-enum` is read as `[level, 'always', [types]]`, the only accepted types; `'never'` forbids the types it lists (a release rule or section for one is reported) and enumerates none, so nothing is required of any other type, and level 0 accepts every type; any other shape of the rule fails the check; `releaseRules` given as a module path (a string, which semantic-release supports) is not read and fails the check; types that come only from an extended preset are not read; rules that name no type (`{ breaking: true }`) are ignored; a changelog list that omits a preset type is not reported; the configs are executed, so they must be trusted.
161
161
 
162
+ **`engines-floor`** verifies that 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, so the package admits no Node version that something it needs at run time rejects. A floor comparison is not enough: a package declaring `>=20` over a dependency requiring `^20.19.0 || ^22.13.0 || >=24` admits 20.0 to 20.18, 21 and 23, which the dependency rejects, so the test is `semver.subset(package, dependency)`. Option: `packages`, globs of the package directories in the form of the `packages` list of `pnpm-workspace.yaml` (a `!` pattern excludes, `.` is the root package); the root package and the `packages` of `pnpm-workspace.yaml`, when it exists, when omitted. Codes: `engines-floor/wider-than-dependency` (the range admits a version the dependency rejects) and `no-engines` (the package declares no range, so admits every version, while a dependency declares one), each reported at the package's `package.json` and naming the dependency and both ranges. A dependency that declares no range constrains nothing and is ignored. `devDependencies` are not judged: a consumer never installs them, and the Node the development toolchain needs is the repository's own concern, not a claim the published package makes. A dependency is found the way Node finds a package directory, in `node_modules` of the package's directory and then of each directory above it up to the working directory, following the symbolic links pnpm installs; it reads the dependency's `package.json` directly rather than resolving an entry point, since an `exports` map need not expose `package.json`. A peer dependency is judged only when its range is a single version (`1.2.3` or `=1.2.3`), by the installed copy, which must be that version or the run fails. The consumer, not the package, chooses which version of a peer to install, so the package's range holds when each Node version in it is accepted by some version the peer range admits. The copy installed in the workspace is one of those versions: if it rejects a Node version the package admits, another admitted version may accept it (`^9.0.0 || ^10.0.0` with cosmiconfig 10 requiring `^22.18 || >= 24` while cosmiconfig 9 accepts 22.13, and equally an earlier minor of a single major), and the versions that are not installed cannot be read without the registry, so judging the range by that copy reports packages that are correct. A range of one version leaves the consumer no choice, so its installed copy decides; a peer that is not judged need not be installed. A dependency that is not installed fails the run with a configuration error, because its range cannot be read; an optional dependency, or a peer dependency marked optional in `peerDependenciesMeta`, may be absent and is then skipped. A range semver cannot parse, in the package or a dependency, and options that select no package also fail the run. Limits: 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, and a set that two adjacent dependency sets only cover together (`>=22` against `^22.0.0 || >=23`) is reported until it is split the same way; a peer dependency whose range admits more than one version is not judged at all, so a package can claim a Node version that no admitted version of a peer accepts; prerelease Node versions are not considered.
163
+
162
164
  **`dockerfile-package-manager`** verifies that a package manager version pinned in a Dockerfile is the one `packageManager` names, ignoring a `+sha` suffix on either side. Options: `dockerfiles` (`Dockerfile`, `Dockerfile.*` and `*.Dockerfile` at any depth), `exclude` (globs left out of the search) and `packageJson` (`package.json`). Only pins of the package manager `packageManager` names are compared: `pnpm@12.4.1` on a `RUN` line is compared, `npm@latest` is not. Codes: `dockerfile-package-manager/version-mismatch` (with line and column) and `no-package-manager`. A version that refers to a variable (`pnpm@${PNPM_VERSION}` or `pnpm@$PNPM_VERSION`) is compared with the value an `ARG` or `ENV` earlier in the same file gives it. Limits: it reads `<manager>@<version>` as written on a line that is not a comment; a variable the file gives no value (an `ARG` without a default, whose value the build supplies) or whose value refers to another variable is not compared; the value in effect is the latest one set before the pin, whichever build stage set it; and an unpinned `pnpm@latest` is a mismatch.
163
165
 
164
166
  **`migrations-directory`** verifies that the directory a schema generator writes migrations to is the directory the deploy tool applies them from, and that no `package.json` script applies them with the generator in place of the deploy tool. It is tool specific, so the generator and the deploy tool are named in the options, each has an adapter that knows its config file names and where in the config the directory is, and the supported pair is `drizzle-kit` (`out` of `drizzle.config.ts`, `.js` or `.json`, `drizzle` when unset) with `wrangler` (`migrations_dir` of a `d1_databases` entry of `wrangler.json`, `.jsonc` or `.toml`, `migrations` when unset). Both directories are resolved against the directory of the file that sets them, and `..` segments and trailing separators are normalised. Adapters are two small interfaces (`GeneratorAdapter` and `DeployToolAdapter`, with `generators` and `deployTools` as the registries), so another tool is one object and an entry in `generatorNames` or `deployToolNames`.
@@ -224,6 +226,7 @@ Every workflow check takes `workflows` (globs of the files to read, `.github/wor
224
226
  | `workflow-runner-resolution` | A custom or self-hosted runner label is not named literally in several jobs; a resolver job has a `timeout-minutes` and its output a literal hosted fallback. | `repeated-label`, `resolver-no-timeout`, `resolver-no-fallback`, `resolver-fallback-not-hosted` |
225
227
  | `workflow-version-single-source` | A setup step does not hold a literal runtime version when `.nvmrc` or `.tool-versions` already does. | `literal-version` |
226
228
  | `workflow-credentials` | Attestation steps have `id-token: write` and `attestations: write`; a publish that passes no token blanks the token variables, or at least requests provenance so that a fallback publish is traceable. | `attestation-permissions`, `tokenless-publish-unprotected` |
229
+ | `workflow-release-job` | The release job is the only job granted `id-token: write`, runs in a deployment environment and checks out with `persist-credentials: false`; no job holding `id-token: write` passes an npm token from a secret. | `id-token-workflow-level`, `id-token-outside-release`, `missing-environment`, `persisted-credentials`, `token-beside-oidc` |
227
230
  | `workflow-repository-dispatch` | A `repository_dispatch` handler does not push to the default branch, auto-merge without a required-checks rule, push or open pull requests as `GITHUB_TOKEN`, or dispatch to another repository with it. | `pushes-default-branch`, `auto-merge-without-required-checks`, `default-token-triggers-nothing`, `dispatch-other-repository-with-default-token` |
228
231
  | `workflow-update-bot-cooldown` | Dependabot has a `cooldown` in every update entry, and Renovate sets `minimumReleaseAge`. | `dependabot-no-cooldown`, `renovate-no-minimum-release-age` |
229
232
  | `workflow-merge-group` | A workflow that runs for `pull_request` also has a `merge_group` trigger. | `missing-trigger` |
@@ -241,6 +244,10 @@ Codes are `<check>/<code>`, so `workflow-job-ordering/release-before-deploy`. Th
241
244
 
242
245
  **`workflow-credentials`**: an attestation step (`actions/attest`, `actions/attest-build-provenance`, `actions/attest-sbom`) needs `id-token: write` and `attestations: write` in the job's effective permissions. Permissions declared nowhere are the repository's default, which the file cannot show and which does not grant `id-token`, so they are reported. A job with `id-token: write` that runs a publish command (`npm`, `pnpm` or `yarn npm publish`, with options such as `-r` or `--filter <package>` before `publish`, `semantic-release`, `changeset publish`, `lerna publish`, bare or behind `pnpm`, `pnpm exec`, `yarn` or `npx`) and passes no token must set `NODE_AUTH_TOKEN` and `NPM_TOKEN` to the empty string (at step, job or workflow level) or request provenance (`--provenance`, `NPM_CONFIG_PROVENANCE: true`, or `publishConfig.provenance: true` in the `package.json` of the working directory); provenance does not stop an inherited token being used, it makes that publish traceable. Limits: commands are read as text where a command starts, so a script file or composite action is not seen; provenance set in another `package.json` is not read; a job is tokenless only if no token variable has a non-empty value anywhere in it; a job that calls a reusable workflow is not read. `registry-url` is deliberately not reported, see below.
243
246
 
247
+ **`workflow-release-job`** takes `releaseJobs` (`release`, the same default as `workflow-job-ordering`). npm binds a trusted publisher to the repository and the workflow file, not to a branch ([trusted publishers](https://docs.npmjs.com/trusted-publishers), [archived](https://web.archive.org/web/20260921070919/https://docs.npmjs.com/trusted-publishers/)), so anyone who can push a branch can edit that branch's copy of the workflow, remove the `if` that keeps publishing to the default branch, and publish. The protection has to live outside the file: a deployment environment whose deployment branch policy allows only the default branch, named as the environment of the trusted publisher on npm, so an OIDC token minted anywhere else is refused. In a workflow with a release job, `id-token-workflow-level` reports workflow-level `permissions` that grant `id-token: write` (directly or through `write-all`), since every job that declares none inherits it, and `id-token-outside-release` reports any other job that declares it, a job that calls a reusable workflow included, because the OIDC identity is the publishing credential and only the release job should be able to mint it. `missing-environment` reports a release job with no `environment` (a name, or a mapping with a `name`). `persisted-credentials` reports a release job's `actions/checkout` that does not set `persist-credentials: false` literally, since the job token, a `token` input or an `ssh-key` would otherwise stay in git config for every later step, the dependency install and third-party actions included. There is no exception for a release that pushes: semantic-release pushes to its `repositoryUrl` as written and, only when that fails, to the same URL with `GITHUB_TOKEN` or `GH_TOKEN` from the environment embedded (`lib/get-git-auth-url.js` of `semantic-release`), so it never needs the checkout's credentials. A deploy-key release job, the shape ExaDev's own release jobs use, checks out with `persist-credentials: false`, writes the key to a file in the step before the release, and hands it to git only through the `GIT_SSH_COMMAND` in the `env` of the steps that push; it is accepted as written, and the clean fixture holds one. In every workflow, `token-beside-oidc` reports a job whose effective permissions grant `id-token: write` and whose workflow, job or step `env` sets `NODE_AUTH_TOKEN` or `NPM_TOKEN` from the `secrets` context: a long-lived token beside the OIDC identity is a second way to publish that no environment protection or trusted publisher setting covers. Limits: the environment's protection rules and the npm trusted publisher settings cannot be seen in the file and are not read (`missing-environment` checks only that an environment is named); a release job that calls a reusable workflow is judged only for `id-token`, since `environment` and the steps belong to the called workflow; a token passed through `with`, written to an `.npmrc` by a script, or set inside a composite action is not seen.
248
+
249
+ What zizmor already covers, and what this check adds, as observed with zizmor 1.30.1 on this check's fixtures ([audit rules](https://docs.zizmor.sh/audits/), [archived](https://web.archive.org/web/20260929212113/https://docs.zizmor.sh/audits/)): `excessive-permissions` reports a workflow-level `id-token: write`, but not `id-token: write` declared by a second job, which only `undocumented-permissions` mentions when the grant has no comment. `artipacked` reports every `actions/checkout` without `persist-credentials: false`, in every job and every persona, at `help` severity for `actions/checkout` v6 and later, which keeps the credentials under `$RUNNER_TEMP` rather than in `.git/config`; this check reports it as a violation in the release job, where a step that reads git config has the publishing job's push credential. `secrets-outside-env` (auditor persona only) reports a `secrets` reference in a job with no environment, so it catches a missing environment only when the release job reads a secret; a tokenless OIDC release job with no environment produces no zizmor finding at all. `use-trusted-publishing` reports an `npm publish` that uses a token, but is silent once the job also has `id-token: write`, so an `NPM_TOKEN` from a secret beside `id-token: write` in a job with an environment produces no zizmor finding. No zizmor audit relates the grants of one job to the others in a workflow, or requires an environment for the job that holds `id-token: write`.
250
+
244
251
  **`workflow-repository-dispatch`** takes `defaultBranches` (`main` and `master`; the `github.event.repository.default_branch` expression always counts) and `assumeRequiredChecks`. It reads workflows with a `repository_dispatch` trigger, which anyone with write access can start through the API. `pushes-default-branch` reports a `git push` whose destination is the default branch (a leading `+` is ignored, a bare `HEAD` is the checked-out branch, quotes around a word are removed, `git -C <directory>` and `git -c <setting>` before `push` are read, and options that take a value such as `-o` are skipped), a push with no refspec while the default branch is checked out (no `git checkout -b` or `git switch -c` earlier in the job, no non-default `ref` on the checkout) and `stefanzweifel/git-auto-commit-action` on the default branch. `auto-merge-without-required-checks` reports `gh pr merge --auto`, which merges at once when the base branch requires no checks; the file cannot show the rule, so `assumeRequiredChecks: true` states it and `settings-required-checks` verifies it. `default-token-triggers-nothing` reports a push whose checkout kept the default token (no `token` input; a push to a URL that carries its own credentials does not use it), `gh pr create` with `GITHUB_TOKEN` and `peter-evans/create-pull-request` with the default token: events created with `GITHUB_TOKEN` start no workflows, so the result is never checked. `dispatch-other-repository-with-default-token` reports a dispatch (`gh api repos/<o>/<r>/dispatches`, `gh workflow run -R`, `peter-evans/repository-dispatch`) to a repository other than `github.repository` with `GITHUB_TOKEN`, whose access is limited to its own repository. Dispatching in the same repository with `GITHUB_TOKEN` is not reported, because GitHub documents `workflow_dispatch` and `repository_dispatch` as the events that do create runs when made with it ([events that trigger workflows](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows), [archived](https://web.archive.org/web/20261001051219/https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows)). Limits: commands are read as text, so a script file is not seen; a dispatch target written literally as this repository's own name is reported, since the file does not know the name.
245
252
 
246
253
  **`workflow-update-bot-cooldown`** takes `dependabot` (`.github/dependabot.yml`, else `.yaml`) and `renovate` (`renovate.json`, `.renovaterc.json`, `.renovaterc` and `.github/renovate.json`, the first that exists). Every Dependabot `updates` entry needs a `cooldown` with `default-days` or one of the `semver-*-days` above zero. Renovate needs `minimumReleaseAge` at the top level or in a `packageRules` entry, as a duration above zero (`0 days` and `null` wait for nothing). Limits: Dependabot's cooldown does not apply to security updates; a Renovate preset is not resolved (an `extends` entry counts only when its name contains `minimumReleaseAge`); a Renovate config is read as JSON with comments and trailing commas, so other JSON5 syntax and a `renovate` key in `package.json` are not read. A repository with neither bot has nothing to report; a file named in the options that does not exist fails the run.
@@ -306,7 +313,7 @@ it('conforms to the workspace layout', async () => {
306
313
  });
307
314
  ```
308
315
 
309
- Every check is also exported as a function taking `{ cwd, options }` (and `layout` for the import checks): `aggregateMappers`, `codecPairs`, `commandTypes`, `derivedTypes`, `importUphill`, `importRankSkip`, `importCrossSlice`, `importIsolatedGroups`, `importCycles`, `instructionSymlinks`, `singleStorybook`, `commitTypes`, `dockerfilePackageManager`, `migrationsDirectory`, `eslint`, the workflow checks (`workflowJobOrdering`, `workflowSkippableJobs`, `workflowRunnerResolution`, `workflowVersionSingleSource`, `workflowCredentials`, `workflowRepositoryDispatch`, `workflowUpdateBotCooldown`, `workflowMergeGroup` and `workflowActionPinning`) and, taking `{ cwd, options, github }`, the settings checks (`settingsMergeMethods`, `settingsRequiredChecks` and `settingsReviewThreadResolution`). These take options and a layout as already validated and do not check them again; go through `runChecks` for validation. `parseWorkflow` and `loadWorkflows` expose the workflow model the workflow checks read. A check is a function that returns `Promise<readonly Violation[]>`, where a `Violation` is `{ code, message, file, location? }`.
316
+ Every check is also exported as a function taking `{ cwd, options }` (and `layout` for the import checks): `aggregateMappers`, `codecPairs`, `commandTypes`, `derivedTypes`, `importUphill`, `importRankSkip`, `importCrossSlice`, `importIsolatedGroups`, `importCycles`, `instructionSymlinks`, `singleStorybook`, `commitTypes`, `enginesFloor`, `dockerfilePackageManager`, `migrationsDirectory`, `eslint`, the workflow checks (`workflowJobOrdering`, `workflowSkippableJobs`, `workflowRunnerResolution`, `workflowVersionSingleSource`, `workflowCredentials`, `workflowReleaseJob`, `workflowRepositoryDispatch`, `workflowUpdateBotCooldown`, `workflowMergeGroup` and `workflowActionPinning`) and, taking `{ cwd, options, github }`, the settings checks (`settingsMergeMethods`, `settingsRequiredChecks` and `settingsReviewThreadResolution`). These take options and a layout as already validated and do not check them again; go through `runChecks` for validation. `parseWorkflow` and `loadWorkflows` expose the workflow model the workflow checks read. A check is a function that returns `Promise<readonly Violation[]>`, where a `Violation` is `{ code, message, file, location? }`.
310
317
 
311
318
  ### Proving a handler map is exhaustive
312
319