workspace-conformance 3.0.0 → 4.0.1

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
@@ -1,6 +1,6 @@
1
1
  # workspace-conformance
2
2
 
3
- Conformance checks for a pnpm workspace that ESLint cannot express. Type-graph checks run on [ts-morph](https://ts-morph.com), import-graph checks run on [dependency-cruiser](https://github.com/sverweij/dependency-cruiser) with rules generated from the shared `layout` section of [`@exadev/config`](https://github.com/ExaDev/config), file-tree and cross-config checks compare two files or read git state, workflow checks read GitHub Actions files, and settings checks read the repository's settings through the GitHub API. Each check reports violations with a stable code, a message, a file and, where it has one, a position.
3
+ Conformance checks for a pnpm workspace that ESLint cannot express. Type-graph checks run on [ts-morph](https://ts-morph.com), import-graph checks run on [dependency-cruiser](https://github.com/sverweij/dependency-cruiser) with rules generated from the shared `layout` section of [`@exadev/config`](https://github.com/ExaDev/config), file-tree and cross-config checks compare two files or read git state, workflow checks read GitHub Actions files, and settings checks read the repository's settings through the GitHub API. Each check reports violations with a stable code, a message and what the violation is about: a file and, where it has one, a position, or for the settings checks the repository.
4
4
 
5
5
  It complements the workspace architecture rules of [`@exadev/eslint-config`](https://github.com/ExaDev/eslint-config), it does not replace them. Those rules run in the editor and check what `package.json` files declare (`no-uphill-dependency` and `no-dependency-cycle`), incrementally and one file at a time. This tool checks what the code does: the import statements that actually cross packages, whether written as a package name, a relative path or a path alias, and everything that needs two files, the type checker or git. Both read the same `layout` section, so one description of the workspace drives both. Naming (`package-name-mirrors-path`) and per-file lint stay in ESLint.
6
6
 
@@ -257,7 +257,7 @@ What zizmor already covers, and what this check adds, as observed with zizmor 1.
257
257
 
258
258
  **`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.
259
259
 
260
- **`workflow-merge-group`** reports a workflow with a `pull_request` trigger and no `merge_group` trigger: GitHub starts no run of it for a queued pull request, so its required checks never report. Whether the branch has a merge queue at all is a repository setting. When the run is given a GitHub client (`runChecks({ github })`, `--settings`), the check reads the rules of the branch, as the `settings-*` checks do and with the same `repository` and `branch` options, once it has found a workflow without the trigger, and reports only when a `merge_queue` rule applies; without a queue no `merge_group` event is sent, so the trigger is not needed. Offline it cannot tell, and it reports every such workflow, as it always has. The file cannot show which checks are required, so a `pull_request` workflow whose checks are not required is reported too; narrow it with `workflows` and `exclude`. `pull_request_target` is left out, since it is not a place for required checks, and a workflow with no `pull_request` trigger is not judged. Limits: a `merge_group` trigger takes only `branches` filters (`paths` is a filter of `push`, `pull_request` and `pull_request_target`), so a workflow path-filtered for pull requests still runs in the queue once it has the trigger, and the `branches` filter of a `merge_group` trigger is not read ([workflow syntax](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax)); with a client only rulesets are read, so a merge queue configured through classic branch protection is not seen and nothing is reported.
260
+ **`workflow-merge-group`** reports a workflow with a `pull_request` trigger and no `merge_group` trigger: GitHub starts no run of it for a queued pull request, so its required checks never report. Whether the branch has a merge queue at all is a repository setting. When the run is given a GitHub client (`runChecks({ github })`, `--settings`), the check reads the rules of the branch, as the `settings-*` checks do and with the same `repository` and `branch` options, once it has found a workflow without the trigger, and reports only when a `merge_queue` rule applies; without a queue no `merge_group` event is sent, so the trigger is not needed. Offline it cannot tell, and it reports every such workflow, as it always has. The file cannot show which checks are required, so a `pull_request` workflow whose checks are not required is reported too; narrow it with `workflows` and `exclude`. `pull_request_target` is left out, since it is not a place for required checks, and a workflow with no `pull_request` trigger is not judged. Limits: a `merge_group` trigger takes only `branches` filters (`paths` is a filter of `push`, `pull_request` and `pull_request_target`), so a workflow path-filtered for pull requests still runs in the queue once it has the trigger, and the `branches` filter of a `merge_group` trigger is not read ([workflow syntax](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax)); with a client only rulesets are read, so a merge queue configured through classic branch protection is not seen and nothing is reported. When GitHub refuses the rules because the plan of the repository has no rulesets (the refusal the `settings-*` checks report as `rules-unavailable`, and `RulesetsUnavailableError` from a client of your own), nothing is reported either, since the repository cannot have a merge queue: GitHub offers merge queues only in public repositories owned by an organisation and in private ones of organisations on GitHub Enterprise Cloud, which have rulesets ([managing a merge queue](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue), [archived](https://web.archive.org/web/20261002170053/https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue)); any other refusal fails the run.
261
261
 
262
262
  **`workflow-action-pinning`** encodes a pinning policy, and does not choose a side silently: pinning to a commit SHA protects against a moved tag or branch, and a ref lets the owner of a shared workflow roll out a fix without every caller changing. The default follows the reasoning that you do not control a third party but do control your own organisation: an action or reusable workflow from outside `organisations` must be pinned by full 40-character commit SHA (`thirdParty: 'sha'`), a Docker image by digest, and one owned by a listed organisation may use any ref (`sameOrganisation: 'ref'`). With no `organisations` configured, nothing is relaxed. `thirdParty` and `sameOrganisation` each take `sha` or `ref`, so either side of the policy can be flipped, `organisations` lists the owners that count as the same organisation, and `allow` exempts `owner/repository` or `owner/*` (for example `actions/*`). Local actions and workflows (`./...`) are not judged. Limits: it checks the form of the ref, not that the SHA belongs to the repository or to a release; composite actions are not read.
263
263
 
@@ -269,7 +269,7 @@ What zizmor already covers, and what this check adds, as observed with zizmor 1.
269
269
 
270
270
  ### Repository settings
271
271
 
272
- The `settings-*` checks read repository settings through the GitHub API, so they need a token and are kept apart from the file checks. They run only when the run is given a GitHub client: `runChecks({ github })` in the library, `--settings` on the command line (which reads the token from `GITHUB_TOKEN` or `GH_TOKEN`). Without one they are left out, even when enabled, so the same configuration runs offline; naming one with `--check` or `checks` without a client, or enabling only settings checks and running offline, fails the run instead of reporting a pass. The client is an interface (`GitHubClient`, with `repository` and `branchRules`, whose `BranchRules` result also says whether a `required_linear_history` and a `merge_queue` rule apply); `workflow-merge-group` reads it too when it is given, and runs without it; `createGitHubClient({ token })` is the implementation over the REST API (GitHub reports the merge-method settings only to a token with write access to the repository, so `repository` fails with a `ConformanceError` for a read-only token), and a test or another host can supply its own. A violation's `file` is the repository, `owner/name`.
272
+ The `settings-*` checks read repository settings through the GitHub API, so they need a token and are kept apart from the file checks. They run only when the run is given a GitHub client: `runChecks({ github })` in the library, `--settings` on the command line (which reads the token from `GITHUB_TOKEN` or `GH_TOKEN`). Without one they are left out, even when enabled, so the same configuration runs offline; naming one with `--check` or `checks` without a client, or enabling only settings checks and running offline, fails the run instead of reporting a pass. The client is an interface (`GitHubClient`, with `repository` and `branchRules`, whose `BranchRules` result also says whether a `required_linear_history` and a `merge_queue` rule apply); `workflow-merge-group` reads it too when it is given, and runs without it; `createGitHubClient({ token })` is the implementation over the REST API (GitHub reports the merge-method settings only to a token with write access to the repository, so `repository` fails with a `ConformanceError` for a read-only token), and a test or another host can supply its own. A settings violation is about the repository, not a file in it, so it has `repository` (`owner/name`) in place of `file` and no `location`.
273
273
 
274
274
  All three take `repository` (`owner/name`; the `origin` remote of the working directory, as configured before any `insteadOf` rewriting, when omitted) and `branch` (the repository's default branch). Rules are the effective rules of the branch from `GET /repos/{owner}/{repo}/rules/branches/{branch}` ([docs](https://docs.github.com/en/rest/repos/rules), [archived](https://web.archive.org/web/20260924024404/https://docs.github.com/en/rest/repos/rules)), which covers rulesets and not classic branch protection.
275
275
 
@@ -293,9 +293,9 @@ workspace-conformance check [--cwd <directory>] [--check <name>]... [--list] [--
293
293
 
294
294
  | Format | Output |
295
295
  |---|---|
296
- | `text` | The default. Violations on standard error as `file:line:column: code: message` (`file: code: message` for one without a position), a check's notes on standard output as `check: note`, and on a clean run a line naming the checks that ran. |
297
- | `json` | One JSON document on standard output: the `RunResult` of `runChecks`, so `results` (each `{ check, violations, notes }`), `violations` (the library's `{ code, message, file, location? }` objects, with `file` relative to `--cwd`) and `exitCode`. |
298
- | `github` | A GitHub Actions error annotation per violation on standard output, `::error file=<file>,line=<line>,col=<column>,title=<code>::<message>` (no `line` and `col` without a position), and a `::notice` per note. The runner turns each into an annotation on the file ([workflow commands](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#setting-an-error-message), [archived](https://web.archive.org/web/20260918001346/https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands)). `file` is relative to the directory the command runs in, the repository root in a workflow step unless it sets `working-directory`, so `--cwd` may name a subdirectory. Reserved characters are percent-encoded as `@actions/core` encodes them (`%`, carriage return and line feed everywhere, and `:` and `,` in properties; [`command.ts`](https://github.com/actions/toolkit/blob/main/packages/core/src/command.ts), [archived](https://web.archive.org/web/20250901085906/https://github.com/actions/toolkit/blob/main/packages/core/src/command.ts)). A settings violation names the repository rather than a file, so its annotation is attached to no file. |
296
+ | `text` | The default. Violations on standard error as `file:line:column: code: message` (`file: code: message` for one without a position, `owner/name: code: message` for a settings violation), a check's notes on standard output as `check: note`, and on a clean run a line naming the checks that ran. |
297
+ | `json` | One JSON document on standard output: the `RunResult` of `runChecks`, so `results` (each `{ check, violations, notes }`), `violations` (the library's `{ code, message, file, location? }` objects, with `file` relative to `--cwd`, and `{ code, message, repository }` for a settings violation) and `exitCode`. |
298
+ | `github` | A GitHub Actions error annotation per violation on standard output, `::error file=<file>,line=<line>,col=<column>,title=<code>::<message>` (no `line` and `col` without a position), and a `::notice` per note. The runner turns each into an annotation on the file ([workflow commands](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#setting-an-error-message), [archived](https://web.archive.org/web/20260918001346/https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands)). `file` is relative to the directory the command runs in, the repository root in a workflow step unless it sets `working-directory`, so `--cwd` may name a subdirectory. Reserved characters are percent-encoded as `@actions/core` encodes them (`%`, carriage return and line feed everywhere, and `:` and `,` in properties; [`command.ts`](https://github.com/actions/toolkit/blob/main/packages/core/src/command.ts), [archived](https://web.archive.org/web/20250901085906/https://github.com/actions/toolkit/blob/main/packages/core/src/command.ts)). A settings violation names the repository rather than a file, so its annotation has no `file` and is attached to no file: `::error title=<code>::<message>`. |
299
299
 
300
300
  When the checks cannot run (exit status 2), the reason is printed to standard error as text in every format.
301
301
 
@@ -328,7 +328,7 @@ it('conforms to the workspace layout', async () => {
328
328
  });
329
329
  ```
330
330
 
331
- 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`); `workflowMergeGroup` takes `github` too, optionally. 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? }`.
331
+ 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`); `workflowMergeGroup` takes `github` too, optionally. 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 either a `FileViolation`, `{ code, message, file, location? }`, or, from a settings check, a `RepositoryViolation`, `{ code, message, repository }`; exactly one of `file` and `repository` is present, so `violation.file === undefined` tells them apart.
332
332
 
333
333
  ### Proving a handler map is exhaustive
334
334
 
@@ -379,6 +379,8 @@ Each check has a fixture that violates it and one that does not, under `test/fix
379
379
 
380
380
  Every push to `main` runs semantic-release once the commitlint, lint, typecheck and package jobs pass. It derives the next version from the Conventional Commits since the last tag, publishes to npm through trusted publishing with provenance, creates the tag and the GitHub Release, and pushes a `chore(release)` commit that updates `CHANGELOG.md` and `package.json`. `main` is protected by a ruleset that requires the Required Checks status, a branch up to date with `main`, linear history, and forbids deletion and non-fast-forward pushes. Pull requests therefore reach `main` by rebase merge once checks pass, and the release commit and tag are pushed over SSH with the repository deploy key, which the ruleset lets bypass it (as it does organisation admins). The key exists on disk only for the release step.
381
381
 
382
+ CI also lints the workflows themselves with [actionlint](https://github.com/rhysd/actionlint) (and the shellcheck it runs on every `run:` script) and [zizmor](https://docs.zizmor.sh), each at a pinned version and configured in `.github/actionlint.yaml` and `.github/zizmor.yml`; both are part of the Required Checks status.
383
+
382
384
  CI selects its runner with `ExaDev/runner-fallback-action` (self-hosted fleet first, Blacksmith as fallback). The release job stays on a GitHub-hosted runner because npm trusted publishing needs one, and publishes with provenance through OIDC, with no stored token. The repository deploy key (the `RELEASE_DEPLOY_KEY` secret) is a ruleset bypass actor, so it is written to disk by the step immediately before the release step, and only the release step's own environment carries `GIT_SSH_COMMAND` (absolute `/usr/bin/ssh`, no ssh config, a pinned GitHub host key, only the ssh protocol allowed); the key is deleted afterwards. The build and package checks run before the key exists and the release step sets `NPM_CONFIG_IGNORE_SCRIPTS`, so no dev dependency runs beside the key, the job's actions are pinned by commit SHA, and the checkout, the action setup and the dependency install run with no write-capable credential in git config. That limits what code from those steps can read; it is not a sandbox, since a process started earlier in the job still runs as the same user. `release.config.ts` sets an SSH `repositoryUrl`, so semantic-release pushes the release commit and tag over SSH. The default `GITHUB_TOKEN` cannot bypass a branch ruleset on `main`, and semantic-release falls back to pushing as it only when the SSH push fails, so the job dry-run pushes first and fails before publishing if the key does not authenticate, and refuses the https URL outright. Commits follow [Conventional Commits](https://www.conventionalcommits.org); semantic-release derives the version from them.
383
385
 
384
386
  `README.md`, `AGENTS.md` and `CLAUDE.md` are one file: the two instruction files are tracked symlinks to this one.
package/dist/cli.js CHANGED
@@ -1323,9 +1323,9 @@ async function branchOf(github, slug, options) {
1323
1323
  return options.branch ?? (await github.repository(slug)).defaultBranch;
1324
1324
  }
1325
1325
  /**
1326
- * What a violation of a settings check names as its file.
1326
+ * The repository a violation of a settings check names, `owner/name`.
1327
1327
  */
1328
- function repositoryFile(slug) {
1328
+ function repositoryName(slug) {
1329
1329
  return `${slug.owner}/${slug.name}`;
1330
1330
  }
1331
1331
  /**
@@ -1343,8 +1343,8 @@ async function branchRulesOf(github, slug, options) {
1343
1343
  /**
1344
1344
  * The message of a `rules-unavailable` violation: the branch rules of the repository cannot be read or enforced on its plan, so `what` cannot be required.
1345
1345
  */
1346
- function rulesUnavailableMessage(file, what, githubMessage) {
1347
- return `the branch rules of ${file} cannot be read or enforced on its plan, so ${what} cannot be required (GitHub: ${githubMessage})`;
1346
+ function rulesUnavailableMessage(repository, what, githubMessage) {
1347
+ return `the branch rules of ${repository} cannot be read or enforced on its plan, so ${what} cannot be required (GitHub: ${githubMessage})`;
1348
1348
  }
1349
1349
  //#endregion
1350
1350
  //#region src/checks/settings-merge-methods.ts
@@ -1370,7 +1370,7 @@ const METHOD_NAMES = {
1370
1370
  */
1371
1371
  const settingsMergeMethods = async ({ cwd, options, github }) => {
1372
1372
  const slug = await repositoryOf(cwd, options);
1373
- const file = repositoryFile(slug);
1373
+ const repository = repositoryName(slug);
1374
1374
  const settings = await github.repository(slug);
1375
1375
  const read = await branchRulesOf(github, slug, options);
1376
1376
  const pullRequests = "unavailable" in read ? [] : read.rules.pullRequests;
@@ -1385,13 +1385,13 @@ const settingsMergeMethods = async ({ cwd, options, github }) => {
1385
1385
  const available = METHODS.filter((method) => repositoryAllows[method] && rulesAllow(method));
1386
1386
  const violations = available.filter((method) => method !== wanted).map((method) => ({
1387
1387
  code: "settings-merge-methods/method-enabled",
1388
- message: `${file} allows ${METHOD_NAMES[method]}; only ${METHOD_NAMES[wanted]} should be allowed`,
1389
- file
1388
+ message: `${repository} allows ${METHOD_NAMES[method]}; only ${METHOD_NAMES[wanted]} should be allowed`,
1389
+ repository
1390
1390
  }));
1391
1391
  if (!available.includes(wanted)) violations.push({
1392
1392
  code: "settings-merge-methods/method-unavailable",
1393
- message: `${file} does not allow ${METHOD_NAMES[wanted]}`,
1394
- file
1393
+ message: `${repository} does not allow ${METHOD_NAMES[wanted]}`,
1394
+ repository
1395
1395
  });
1396
1396
  return violations;
1397
1397
  };
@@ -1789,12 +1789,12 @@ const REGISTRY_TOKEN_VARIABLES = ["NODE_AUTH_TOKEN", "NPM_TOKEN"];
1789
1789
  */
1790
1790
  const settingsRequiredChecks = async ({ cwd, options, github }) => {
1791
1791
  const slug = await repositoryOf(cwd, options);
1792
- const file = repositoryFile(slug);
1792
+ const repository = repositoryName(slug);
1793
1793
  const read = await branchRulesOf(github, slug, options);
1794
1794
  if ("unavailable" in read) return [{
1795
1795
  code: "settings-required-checks/rules-unavailable",
1796
- message: rulesUnavailableMessage(file, "a status check", read.unavailable),
1797
- file
1796
+ message: rulesUnavailableMessage(repository, "a status check", read.unavailable),
1797
+ repository
1798
1798
  }];
1799
1799
  const { rules } = read;
1800
1800
  const junctions = (await loadWorkflows(cwd, options)).flatMap((workflow) => jobsNamed(workflow, options.junctionJobs ?? DEFAULT_JUNCTION_JOBS).map((job) => ({
@@ -1805,26 +1805,26 @@ const settingsRequiredChecks = async ({ cwd, options, github }) => {
1805
1805
  if (junctions.length === 0) violations.push({
1806
1806
  code: "settings-required-checks/no-junction-job",
1807
1807
  message: "no workflow has a junction job, so there is no check that reports for the whole workflow to require",
1808
- file
1808
+ repository
1809
1809
  });
1810
1810
  if (rules.requiredStatusChecks.length === 0) {
1811
1811
  violations.push({
1812
1812
  code: "settings-required-checks/no-required-checks",
1813
- message: `no rule requires status checks on the branch of ${file}`,
1814
- file
1813
+ message: `no rule requires status checks on the branch of ${repository}`,
1814
+ repository
1815
1815
  });
1816
1816
  return violations;
1817
1817
  }
1818
1818
  const required = new Set(rules.requiredStatusChecks.flatMap((rule) => rule.contexts));
1819
1819
  for (const { workflow, name } of junctions.filter((junction) => !required.has(junction.name))) violations.push({
1820
1820
  code: "settings-required-checks/required-check-missing",
1821
- message: `${file} does not require the check '${name}' of ${workflow.file}`,
1822
- file
1821
+ message: `${repository} does not require the check '${name}' of ${workflow.file}`,
1822
+ repository
1823
1823
  });
1824
1824
  if (!rules.requiredStatusChecks.some((rule) => rule.strict)) violations.push({
1825
1825
  code: "settings-required-checks/not-strict",
1826
- message: `${file} does not require branches to be up to date before merging`,
1827
- file
1826
+ message: `${repository} does not require branches to be up to date before merging`,
1827
+ repository
1828
1828
  });
1829
1829
  return violations;
1830
1830
  };
@@ -1840,16 +1840,16 @@ const settingsRequiredChecks = async ({ cwd, options, github }) => {
1840
1840
  const settingsReviewThreadResolution = async ({ cwd, options, github }) => {
1841
1841
  const slug = await repositoryOf(cwd, options);
1842
1842
  const read = await branchRulesOf(github, slug, options);
1843
- const file = repositoryFile(slug);
1843
+ const repository = repositoryName(slug);
1844
1844
  if ("unavailable" in read) return [{
1845
1845
  code: "settings-review-thread-resolution/rules-unavailable",
1846
- message: rulesUnavailableMessage(file, "resolving review conversations", read.unavailable),
1847
- file
1846
+ message: rulesUnavailableMessage(repository, "resolving review conversations", read.unavailable),
1847
+ repository
1848
1848
  }];
1849
1849
  return read.rules.pullRequests.some((rule) => rule.requiredReviewThreadResolution) ? [] : [{
1850
1850
  code: "settings-review-thread-resolution/not-required",
1851
- message: `no rule requires review conversations to be resolved before merging on the branch of ${file}`,
1852
- file
1851
+ message: `no rule requires review conversations to be resolved before merging on the branch of ${repository}`,
1852
+ repository
1853
1853
  }];
1854
1854
  };
1855
1855
  //#endregion
@@ -2277,7 +2277,7 @@ const workflowJobOrdering = async ({ cwd, options }) => (await loadWorkflows(cwd
2277
2277
  *
2278
2278
  * A workflow is taken to produce required checks when it is triggered by `pull_request`; `pull_request_target` is left out, since it runs trusted code against untrusted input and is not a place for required checks. The file cannot show which checks are required, so a `pull_request` workflow whose checks are not required is reported too: narrow the check with `workflows` and `exclude`. A `merge_group` trigger on a workflow with no `pull_request` trigger is not judged.
2279
2279
  *
2280
- * Whether the branch has a merge queue is a setting, not something a file shows. Given a GitHub client, the check reads the rules of the branch (`branch`, else the default branch, of `repository`, else the `origin` remote) once it has found a workflow without the trigger, and reports only when a `merge_queue` rule applies, since without a queue no `merge_group` event is ever sent. Offline it cannot tell, so it reports every such workflow.
2280
+ * Whether the branch has a merge queue is a setting, not something a file shows. Given a GitHub client, the check reads the rules of the branch (`branch`, else the default branch, of `repository`, else the `origin` remote) once it has found a workflow without the trigger, and reports only when a `merge_queue` rule applies, since without a queue no `merge_group` event is ever sent. A repository whose plan has no rulesets (the client rejects with `RulesetsUnavailableError`) has no merge queue either, since GitHub offers merge queues only in public repositories of an organisation and in private ones on GitHub Enterprise Cloud, both of which have rulesets, so nothing is reported for it; any other refusal fails the run. Offline it cannot tell, so it reports every such workflow.
2281
2281
  *
2282
2282
  * Limits: a `merge_group` trigger takes only `branches` filters (`paths` is a filter of `push`, `pull_request` and `pull_request_target`), so a workflow path-filtered for pull requests still runs in the queue once it has the trigger; the `branches` filter of a `merge_group` trigger is not read; only rulesets are read, so a queue configured through classic branch protection is not seen and, with a client, nothing is reported for it.
2283
2283
  */
@@ -2287,8 +2287,8 @@ const workflowMergeGroup = async ({ cwd, options, github }) => {
2287
2287
  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)];
2288
2288
  });
2289
2289
  if (violations.length === 0 || github === void 0) return violations;
2290
- const slug = await repositoryOf(cwd, options);
2291
- return (await github.branchRules(slug, await branchOf(github, slug, options))).mergeQueue ? violations : [];
2290
+ const branch = await branchRulesOf(github, await repositoryOf(cwd, options), options);
2291
+ return "rules" in branch && branch.rules.mergeQueue ? violations : [];
2292
2292
  };
2293
2293
  //#endregion
2294
2294
  //#region src/checks/workflow-release-job.ts
@@ -4376,8 +4376,15 @@ Exit status: ${String(EXIT_CODES.clean)} when nothing is found, ${String(EXIT_CO
4376
4376
  function messageOf(error) {
4377
4377
  return error instanceof Error ? error.message : String(error);
4378
4378
  }
4379
+ /**
4380
+ * Where a violation is, as text: the repository a violation of the settings is about, else the file and, when the violation has one, its position.
4381
+ */
4382
+ function placeOf(violation) {
4383
+ if (violation.file === void 0) return violation.repository;
4384
+ return violation.location === void 0 ? violation.file : `${violation.file}:${String(violation.location.line)}:${String(violation.location.column)}`;
4385
+ }
4379
4386
  function formatViolation(violation) {
4380
- return `${violation.location === void 0 ? violation.file : `${violation.file}:${String(violation.location.line)}:${String(violation.location.column)}`}: ${violation.code}: ${violation.message}\n`;
4387
+ return `${placeOf(violation)}: ${violation.code}: ${violation.message}\n`;
4381
4388
  }
4382
4389
  /**
4383
4390
  * A workflow command property value with the characters the runner reserves percent-encoded, as `escapeProperty` of `@actions/core` encodes them.
@@ -4392,11 +4399,15 @@ function escapeData(value) {
4392
4399
  return value.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
4393
4400
  }
4394
4401
  /**
4395
- * The GitHub Actions `error` workflow command for a violation at `file` (relative to the directory the job runs in, which is how the runner places an annotation), with the violation's code as its title and its position when it has one.
4402
+ * The GitHub Actions `error` workflow command for a violation, with its code as the title. A violation about a file is placed at its `file`, which must be relative to the directory the job runs in (that is how the runner places an annotation), and at its position when it has one; a violation about the repository names no file, so its annotation is attached to none.
4396
4403
  */
4397
- function githubAnnotation(violation, file) {
4398
- const position = violation.location === void 0 ? "" : `,line=${String(violation.location.line)},col=${String(violation.location.column)}`;
4399
- return `::error file=${escapeProperty(file)}${position},title=${escapeProperty(violation.code)}::${escapeData(violation.message)}\n`;
4404
+ function githubAnnotation(violation) {
4405
+ const properties = [`title=${escapeProperty(violation.code)}`];
4406
+ if (violation.file !== void 0) {
4407
+ const position = violation.location === void 0 ? [] : [`line=${String(violation.location.line)}`, `col=${String(violation.location.column)}`];
4408
+ properties.unshift(`file=${escapeProperty(violation.file)}`, ...position);
4409
+ }
4410
+ return `::error ${properties.join(",")}::${escapeData(violation.message)}\n`;
4400
4411
  }
4401
4412
  function formatOf(value) {
4402
4413
  if (value === void 0) return "text";
@@ -4415,7 +4426,10 @@ function printResult(result, format, cwd, output) {
4415
4426
  return;
4416
4427
  }
4417
4428
  for (const entry of result.results) for (const note of entry.notes) output.stdout(format === "github" ? `::notice title=${escapeProperty(entry.check)}::${escapeData(note)}\n` : `${entry.check}: ${note}\n`);
4418
- for (const violation of result.violations) if (format === "github") output.stdout(githubAnnotation(violation, relativePosix(process.cwd(), resolve(cwd, violation.file))));
4429
+ for (const violation of result.violations) if (format === "github") output.stdout(githubAnnotation(violation.file !== void 0 ? {
4430
+ ...violation,
4431
+ file: relativePosix(process.cwd(), resolve(cwd, violation.file))
4432
+ } : violation));
4419
4433
  else output.stderr(formatViolation(violation));
4420
4434
  if (result.violations.length === 0) output.stdout(summaryOf(result));
4421
4435
  }
package/dist/index.cjs CHANGED
@@ -2320,9 +2320,9 @@ async function branchOf(github, slug, options) {
2320
2320
  return options.branch ?? (await github.repository(slug)).defaultBranch;
2321
2321
  }
2322
2322
  /**
2323
- * What a violation of a settings check names as its file.
2323
+ * The repository a violation of a settings check names, `owner/name`.
2324
2324
  */
2325
- function repositoryFile(slug) {
2325
+ function repositoryName(slug) {
2326
2326
  return `${slug.owner}/${slug.name}`;
2327
2327
  }
2328
2328
  /**
@@ -2340,8 +2340,8 @@ async function branchRulesOf(github, slug, options) {
2340
2340
  /**
2341
2341
  * The message of a `rules-unavailable` violation: the branch rules of the repository cannot be read or enforced on its plan, so `what` cannot be required.
2342
2342
  */
2343
- function rulesUnavailableMessage(file, what, githubMessage) {
2344
- return `the branch rules of ${file} cannot be read or enforced on its plan, so ${what} cannot be required (GitHub: ${githubMessage})`;
2343
+ function rulesUnavailableMessage(repository, what, githubMessage) {
2344
+ return `the branch rules of ${repository} cannot be read or enforced on its plan, so ${what} cannot be required (GitHub: ${githubMessage})`;
2345
2345
  }
2346
2346
  //#endregion
2347
2347
  //#region src/checks/settings-merge-methods.ts
@@ -2367,7 +2367,7 @@ const METHOD_NAMES = {
2367
2367
  */
2368
2368
  const settingsMergeMethods = async ({ cwd, options, github }) => {
2369
2369
  const slug = await repositoryOf(cwd, options);
2370
- const file = repositoryFile(slug);
2370
+ const repository = repositoryName(slug);
2371
2371
  const settings = await github.repository(slug);
2372
2372
  const read = await branchRulesOf(github, slug, options);
2373
2373
  const pullRequests = "unavailable" in read ? [] : read.rules.pullRequests;
@@ -2382,13 +2382,13 @@ const settingsMergeMethods = async ({ cwd, options, github }) => {
2382
2382
  const available = METHODS.filter((method) => repositoryAllows[method] && rulesAllow(method));
2383
2383
  const violations = available.filter((method) => method !== wanted).map((method) => ({
2384
2384
  code: "settings-merge-methods/method-enabled",
2385
- message: `${file} allows ${METHOD_NAMES[method]}; only ${METHOD_NAMES[wanted]} should be allowed`,
2386
- file
2385
+ message: `${repository} allows ${METHOD_NAMES[method]}; only ${METHOD_NAMES[wanted]} should be allowed`,
2386
+ repository
2387
2387
  }));
2388
2388
  if (!available.includes(wanted)) violations.push({
2389
2389
  code: "settings-merge-methods/method-unavailable",
2390
- message: `${file} does not allow ${METHOD_NAMES[wanted]}`,
2391
- file
2390
+ message: `${repository} does not allow ${METHOD_NAMES[wanted]}`,
2391
+ repository
2392
2392
  });
2393
2393
  return violations;
2394
2394
  };
@@ -2786,12 +2786,12 @@ const REGISTRY_TOKEN_VARIABLES = ["NODE_AUTH_TOKEN", "NPM_TOKEN"];
2786
2786
  */
2787
2787
  const settingsRequiredChecks = async ({ cwd, options, github }) => {
2788
2788
  const slug = await repositoryOf(cwd, options);
2789
- const file = repositoryFile(slug);
2789
+ const repository = repositoryName(slug);
2790
2790
  const read = await branchRulesOf(github, slug, options);
2791
2791
  if ("unavailable" in read) return [{
2792
2792
  code: "settings-required-checks/rules-unavailable",
2793
- message: rulesUnavailableMessage(file, "a status check", read.unavailable),
2794
- file
2793
+ message: rulesUnavailableMessage(repository, "a status check", read.unavailable),
2794
+ repository
2795
2795
  }];
2796
2796
  const { rules } = read;
2797
2797
  const junctions = (await loadWorkflows(cwd, options)).flatMap((workflow) => jobsNamed(workflow, options.junctionJobs ?? DEFAULT_JUNCTION_JOBS).map((job) => ({
@@ -2802,26 +2802,26 @@ const settingsRequiredChecks = async ({ cwd, options, github }) => {
2802
2802
  if (junctions.length === 0) violations.push({
2803
2803
  code: "settings-required-checks/no-junction-job",
2804
2804
  message: "no workflow has a junction job, so there is no check that reports for the whole workflow to require",
2805
- file
2805
+ repository
2806
2806
  });
2807
2807
  if (rules.requiredStatusChecks.length === 0) {
2808
2808
  violations.push({
2809
2809
  code: "settings-required-checks/no-required-checks",
2810
- message: `no rule requires status checks on the branch of ${file}`,
2811
- file
2810
+ message: `no rule requires status checks on the branch of ${repository}`,
2811
+ repository
2812
2812
  });
2813
2813
  return violations;
2814
2814
  }
2815
2815
  const required = new Set(rules.requiredStatusChecks.flatMap((rule) => rule.contexts));
2816
2816
  for (const { workflow, name } of junctions.filter((junction) => !required.has(junction.name))) violations.push({
2817
2817
  code: "settings-required-checks/required-check-missing",
2818
- message: `${file} does not require the check '${name}' of ${workflow.file}`,
2819
- file
2818
+ message: `${repository} does not require the check '${name}' of ${workflow.file}`,
2819
+ repository
2820
2820
  });
2821
2821
  if (!rules.requiredStatusChecks.some((rule) => rule.strict)) violations.push({
2822
2822
  code: "settings-required-checks/not-strict",
2823
- message: `${file} does not require branches to be up to date before merging`,
2824
- file
2823
+ message: `${repository} does not require branches to be up to date before merging`,
2824
+ repository
2825
2825
  });
2826
2826
  return violations;
2827
2827
  };
@@ -2837,16 +2837,16 @@ const settingsRequiredChecks = async ({ cwd, options, github }) => {
2837
2837
  const settingsReviewThreadResolution = async ({ cwd, options, github }) => {
2838
2838
  const slug = await repositoryOf(cwd, options);
2839
2839
  const read = await branchRulesOf(github, slug, options);
2840
- const file = repositoryFile(slug);
2840
+ const repository = repositoryName(slug);
2841
2841
  if ("unavailable" in read) return [{
2842
2842
  code: "settings-review-thread-resolution/rules-unavailable",
2843
- message: rulesUnavailableMessage(file, "resolving review conversations", read.unavailable),
2844
- file
2843
+ message: rulesUnavailableMessage(repository, "resolving review conversations", read.unavailable),
2844
+ repository
2845
2845
  }];
2846
2846
  return read.rules.pullRequests.some((rule) => rule.requiredReviewThreadResolution) ? [] : [{
2847
2847
  code: "settings-review-thread-resolution/not-required",
2848
- message: `no rule requires review conversations to be resolved before merging on the branch of ${file}`,
2849
- file
2848
+ message: `no rule requires review conversations to be resolved before merging on the branch of ${repository}`,
2849
+ repository
2850
2850
  }];
2851
2851
  };
2852
2852
  //#endregion
@@ -3289,7 +3289,7 @@ const workflowJobOrdering = async ({ cwd, options }) => (await loadWorkflows(cwd
3289
3289
  *
3290
3290
  * A workflow is taken to produce required checks when it is triggered by `pull_request`; `pull_request_target` is left out, since it runs trusted code against untrusted input and is not a place for required checks. The file cannot show which checks are required, so a `pull_request` workflow whose checks are not required is reported too: narrow the check with `workflows` and `exclude`. A `merge_group` trigger on a workflow with no `pull_request` trigger is not judged.
3291
3291
  *
3292
- * Whether the branch has a merge queue is a setting, not something a file shows. Given a GitHub client, the check reads the rules of the branch (`branch`, else the default branch, of `repository`, else the `origin` remote) once it has found a workflow without the trigger, and reports only when a `merge_queue` rule applies, since without a queue no `merge_group` event is ever sent. Offline it cannot tell, so it reports every such workflow.
3292
+ * Whether the branch has a merge queue is a setting, not something a file shows. Given a GitHub client, the check reads the rules of the branch (`branch`, else the default branch, of `repository`, else the `origin` remote) once it has found a workflow without the trigger, and reports only when a `merge_queue` rule applies, since without a queue no `merge_group` event is ever sent. A repository whose plan has no rulesets (the client rejects with `RulesetsUnavailableError`) has no merge queue either, since GitHub offers merge queues only in public repositories of an organisation and in private ones on GitHub Enterprise Cloud, both of which have rulesets, so nothing is reported for it; any other refusal fails the run. Offline it cannot tell, so it reports every such workflow.
3293
3293
  *
3294
3294
  * Limits: a `merge_group` trigger takes only `branches` filters (`paths` is a filter of `push`, `pull_request` and `pull_request_target`), so a workflow path-filtered for pull requests still runs in the queue once it has the trigger; the `branches` filter of a `merge_group` trigger is not read; only rulesets are read, so a queue configured through classic branch protection is not seen and, with a client, nothing is reported for it.
3295
3295
  */
@@ -3299,8 +3299,8 @@ const workflowMergeGroup = async ({ cwd, options, github }) => {
3299
3299
  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)];
3300
3300
  });
3301
3301
  if (violations.length === 0 || github === void 0) return violations;
3302
- const slug = await repositoryOf(cwd, options);
3303
- return (await github.branchRules(slug, await branchOf(github, slug, options))).mergeQueue ? violations : [];
3302
+ const branch = await branchRulesOf(github, await repositoryOf(cwd, options), options);
3303
+ return "rules" in branch && branch.rules.mergeQueue ? violations : [];
3304
3304
  };
3305
3305
  //#endregion
3306
3306
  //#region src/checks/workflow-release-job.ts
package/dist/index.d.cts CHANGED
@@ -98,9 +98,9 @@ interface SourceLocation {
98
98
  readonly column: number;
99
99
  }
100
100
  /**
101
- * One way the workspace fails a check.
101
+ * A way the workspace fails a check that is about a file.
102
102
  */
103
- interface Violation {
103
+ interface FileViolation {
104
104
  /**
105
105
  * Stable identifier of the rule that was broken, `<check name>/<reason>`. Callers may match on it; the message may change.
106
106
  */
@@ -111,7 +111,28 @@ interface Violation {
111
111
  */
112
112
  readonly file: string;
113
113
  readonly location?: SourceLocation;
114
+ readonly repository?: never;
114
115
  }
116
+ /**
117
+ * A way the repository fails a check that reads its settings, which is about the repository and no file in it.
118
+ */
119
+ interface RepositoryViolation {
120
+ /**
121
+ * Stable identifier of the rule that was broken, `<check name>/<reason>`. Callers may match on it; the message may change.
122
+ */
123
+ readonly code: string;
124
+ readonly message: string;
125
+ /**
126
+ * The repository the violation is about, `owner/name`.
127
+ */
128
+ readonly repository: string;
129
+ readonly file?: never;
130
+ readonly location?: never;
131
+ }
132
+ /**
133
+ * One way the workspace fails a check: about a file ({@link FileViolation}, with `file`) or about the repository's settings ({@link RepositoryViolation}, with `repository`). A violation has exactly one of the two, so `violation.file === undefined` tells them apart.
134
+ */
135
+ type Violation = FileViolation | RepositoryViolation;
115
136
  /**
116
137
  * What every check receives.
117
138
  */
@@ -139,11 +160,11 @@ interface LayoutCheckContext<Options> extends CheckContext<Options> {
139
160
  /**
140
161
  * A check: reads the workspace and returns everything it finds wrong, in a stable order. It throws `ConformanceError` when it cannot run.
141
162
  */
142
- type CheckFunction<Options> = (context: CheckContext<Options>) => Promise<readonly Violation[]>;
163
+ type CheckFunction<Options> = (context: CheckContext<Options>) => Promise<readonly FileViolation[]>;
143
164
  /**
144
165
  * A check that also needs the resolved workspace layout.
145
166
  */
146
- type LayoutCheckFunction<Options> = (context: LayoutCheckContext<Options>) => Promise<readonly Violation[]>;
167
+ type LayoutCheckFunction<Options> = (context: LayoutCheckContext<Options>) => Promise<readonly FileViolation[]>;
147
168
  /**
148
169
  * What a check that reads repository settings through the GitHub API receives.
149
170
  */
@@ -151,9 +172,9 @@ interface GitHubCheckContext<Options> extends CheckContext<Options> {
151
172
  readonly github: GitHubClient;
152
173
  }
153
174
  /**
154
- * A check that reads repository settings through the GitHub API. Its violations name the repository (`owner/name`) as the file.
175
+ * A check that reads repository settings through the GitHub API. Its violations are about the repository, which they name as `owner/name`.
155
176
  */
156
- type GitHubCheckFunction<Options> = (context: GitHubCheckContext<Options>) => Promise<readonly Violation[]>;
177
+ type GitHubCheckFunction<Options> = (context: GitHubCheckContext<Options>) => Promise<readonly RepositoryViolation[]>;
157
178
  /**
158
179
  * What a check that reads files, and the repository's settings too when it can, receives.
159
180
  */
@@ -166,7 +187,7 @@ interface SettingsAwareCheckContext<Options> extends CheckContext<Options> {
166
187
  /**
167
188
  * A check that reads files and refines its judgement with the repository's settings when the run is given a GitHub client. It runs offline too, unlike a {@link GitHubCheckFunction}.
168
189
  */
169
- type SettingsAwareCheckFunction<Options> = (context: SettingsAwareCheckContext<Options>) => Promise<readonly Violation[]>;
190
+ type SettingsAwareCheckFunction<Options> = (context: SettingsAwareCheckContext<Options>) => Promise<readonly FileViolation[]>;
170
191
  //#endregion
171
192
  //#region src/migrations/adapters.d.ts
172
193
  /**
@@ -1058,7 +1079,7 @@ interface ImportCheckSpec {
1058
1079
  /**
1059
1080
  * The violations for the findings of this check's rules, in a stable order.
1060
1081
  */
1061
- readonly report: (findings: readonly IViolation[], scope: ImportScope) => readonly Violation[];
1082
+ readonly report: (findings: readonly IViolation[], scope: ImportScope) => readonly FileViolation[];
1062
1083
  }
1063
1084
  //#endregion
1064
1085
  //#region src/checks/import-cross-slice.d.ts
@@ -1179,7 +1200,7 @@ export declare const workflowJobOrdering: CheckFunction<WorkflowJobOrderingOptio
1179
1200
  *
1180
1201
  * A workflow is taken to produce required checks when it is triggered by `pull_request`; `pull_request_target` is left out, since it runs trusted code against untrusted input and is not a place for required checks. The file cannot show which checks are required, so a `pull_request` workflow whose checks are not required is reported too: narrow the check with `workflows` and `exclude`. A `merge_group` trigger on a workflow with no `pull_request` trigger is not judged.
1181
1202
  *
1182
- * Whether the branch has a merge queue is a setting, not something a file shows. Given a GitHub client, the check reads the rules of the branch (`branch`, else the default branch, of `repository`, else the `origin` remote) once it has found a workflow without the trigger, and reports only when a `merge_queue` rule applies, since without a queue no `merge_group` event is ever sent. Offline it cannot tell, so it reports every such workflow.
1203
+ * Whether the branch has a merge queue is a setting, not something a file shows. Given a GitHub client, the check reads the rules of the branch (`branch`, else the default branch, of `repository`, else the `origin` remote) once it has found a workflow without the trigger, and reports only when a `merge_queue` rule applies, since without a queue no `merge_group` event is ever sent. A repository whose plan has no rulesets (the client rejects with `RulesetsUnavailableError`) has no merge queue either, since GitHub offers merge queues only in public repositories of an organisation and in private ones on GitHub Enterprise Cloud, both of which have rulesets, so nothing is reported for it; any other refusal fails the run. Offline it cannot tell, so it reports every such workflow.
1183
1204
  *
1184
1205
  * Limits: a `merge_group` trigger takes only `branches` filters (`paths` is a filter of `push`, `pull_request` and `pull_request_target`), so a workflow path-filtered for pull requests still runs in the queue once it has the trigger; the `branches` filter of a `merge_group` trigger is not read; only rulesets are read, so a queue configured through classic branch protection is not seen and, with a client, nothing is reported for it.
1185
1206
  */
@@ -1469,4 +1490,4 @@ export declare function proveExhaustive(input: {
1469
1490
  readonly compilerOptions?: CompilerOptions;
1470
1491
  }): ExhaustivenessProof;
1471
1492
  //#endregion
1472
- export type { AggregateMappersOptions, BranchRules, CaseRemoval, CheckContext, CheckFunction, CheckName, CheckOptionsByName, CheckResult, ChecksConfig, CodecPairsOptions, CommandTypesOptions, CommitTypesOptions, ConformanceConfig, DeployDirectory, DeploySelector, DeployToolAdapter, DeployToolName, DerivedTypesOptions, DerivedTypesPair, DiscoveredPackage, DockerfilePackageManagerOptions, EnginesFloorOptions, EslintOptions, EslintRequiredSeverity, EslintSample, ExhaustivenessProof, GeneratorAdapter, GeneratorName, GitHubCheckContext, GitHubCheckFunction, GitHubClient, GitHubClientOptions, ImportGraphOptions, InstructionSymlinksOptions, Job, LayoutCheckContext, LayoutCheckFunction, MigrationsDirectoryOptions, PermissionAccess, Permissions, PinningLevel, RegisteredCheck, RepositorySettings, RepositorySlug, RunChecksOptions, RunResult, RunsOn, SettingsAwareCheckContext, SettingsAwareCheckFunction, SettingsMergeMethodsOptions, SettingsOptions, SettingsRequiredChecksOptions, SettingsReviewThreadResolutionOptions, SingleStorybookOptions, SourceLocation, Step, TypeErrorReport, UsesReference, Violation, Workflow, WorkflowActionPinningOptions, WorkflowCredentialsOptions, WorkflowJobOrderingOptions, WorkflowMergeGroupOptions, WorkflowReleaseJobOptions, WorkflowRepositoryDispatchOptions, WorkflowRunnerResolutionOptions, WorkflowSelection, WorkflowSkippableJobsOptions, WorkflowUpdateBotCooldownOptions, WorkflowVersionSingleSourceOptions, WorkspacePackage };
1493
+ export type { AggregateMappersOptions, BranchRules, CaseRemoval, CheckContext, CheckFunction, CheckName, CheckOptionsByName, CheckResult, ChecksConfig, CodecPairsOptions, CommandTypesOptions, CommitTypesOptions, ConformanceConfig, DeployDirectory, DeploySelector, DeployToolAdapter, DeployToolName, DerivedTypesOptions, DerivedTypesPair, DiscoveredPackage, DockerfilePackageManagerOptions, EnginesFloorOptions, EslintOptions, EslintRequiredSeverity, EslintSample, ExhaustivenessProof, FileViolation, GeneratorAdapter, GeneratorName, GitHubCheckContext, GitHubCheckFunction, GitHubClient, GitHubClientOptions, ImportGraphOptions, InstructionSymlinksOptions, Job, LayoutCheckContext, LayoutCheckFunction, MigrationsDirectoryOptions, PermissionAccess, Permissions, PinningLevel, RegisteredCheck, RepositorySettings, RepositorySlug, RepositoryViolation, RunChecksOptions, RunResult, RunsOn, SettingsAwareCheckContext, SettingsAwareCheckFunction, SettingsMergeMethodsOptions, SettingsOptions, SettingsRequiredChecksOptions, SettingsReviewThreadResolutionOptions, SingleStorybookOptions, SourceLocation, Step, TypeErrorReport, UsesReference, Violation, Workflow, WorkflowActionPinningOptions, WorkflowCredentialsOptions, WorkflowJobOrderingOptions, WorkflowMergeGroupOptions, WorkflowReleaseJobOptions, WorkflowRepositoryDispatchOptions, WorkflowRunnerResolutionOptions, WorkflowSelection, WorkflowSkippableJobsOptions, WorkflowUpdateBotCooldownOptions, WorkflowVersionSingleSourceOptions, WorkspacePackage };
package/dist/index.d.ts CHANGED
@@ -98,9 +98,9 @@ interface SourceLocation {
98
98
  readonly column: number;
99
99
  }
100
100
  /**
101
- * One way the workspace fails a check.
101
+ * A way the workspace fails a check that is about a file.
102
102
  */
103
- interface Violation {
103
+ interface FileViolation {
104
104
  /**
105
105
  * Stable identifier of the rule that was broken, `<check name>/<reason>`. Callers may match on it; the message may change.
106
106
  */
@@ -111,7 +111,28 @@ interface Violation {
111
111
  */
112
112
  readonly file: string;
113
113
  readonly location?: SourceLocation;
114
+ readonly repository?: never;
114
115
  }
116
+ /**
117
+ * A way the repository fails a check that reads its settings, which is about the repository and no file in it.
118
+ */
119
+ interface RepositoryViolation {
120
+ /**
121
+ * Stable identifier of the rule that was broken, `<check name>/<reason>`. Callers may match on it; the message may change.
122
+ */
123
+ readonly code: string;
124
+ readonly message: string;
125
+ /**
126
+ * The repository the violation is about, `owner/name`.
127
+ */
128
+ readonly repository: string;
129
+ readonly file?: never;
130
+ readonly location?: never;
131
+ }
132
+ /**
133
+ * One way the workspace fails a check: about a file ({@link FileViolation}, with `file`) or about the repository's settings ({@link RepositoryViolation}, with `repository`). A violation has exactly one of the two, so `violation.file === undefined` tells them apart.
134
+ */
135
+ type Violation = FileViolation | RepositoryViolation;
115
136
  /**
116
137
  * What every check receives.
117
138
  */
@@ -139,11 +160,11 @@ interface LayoutCheckContext<Options> extends CheckContext<Options> {
139
160
  /**
140
161
  * A check: reads the workspace and returns everything it finds wrong, in a stable order. It throws `ConformanceError` when it cannot run.
141
162
  */
142
- type CheckFunction<Options> = (context: CheckContext<Options>) => Promise<readonly Violation[]>;
163
+ type CheckFunction<Options> = (context: CheckContext<Options>) => Promise<readonly FileViolation[]>;
143
164
  /**
144
165
  * A check that also needs the resolved workspace layout.
145
166
  */
146
- type LayoutCheckFunction<Options> = (context: LayoutCheckContext<Options>) => Promise<readonly Violation[]>;
167
+ type LayoutCheckFunction<Options> = (context: LayoutCheckContext<Options>) => Promise<readonly FileViolation[]>;
147
168
  /**
148
169
  * What a check that reads repository settings through the GitHub API receives.
149
170
  */
@@ -151,9 +172,9 @@ interface GitHubCheckContext<Options> extends CheckContext<Options> {
151
172
  readonly github: GitHubClient;
152
173
  }
153
174
  /**
154
- * A check that reads repository settings through the GitHub API. Its violations name the repository (`owner/name`) as the file.
175
+ * A check that reads repository settings through the GitHub API. Its violations are about the repository, which they name as `owner/name`.
155
176
  */
156
- type GitHubCheckFunction<Options> = (context: GitHubCheckContext<Options>) => Promise<readonly Violation[]>;
177
+ type GitHubCheckFunction<Options> = (context: GitHubCheckContext<Options>) => Promise<readonly RepositoryViolation[]>;
157
178
  /**
158
179
  * What a check that reads files, and the repository's settings too when it can, receives.
159
180
  */
@@ -166,7 +187,7 @@ interface SettingsAwareCheckContext<Options> extends CheckContext<Options> {
166
187
  /**
167
188
  * A check that reads files and refines its judgement with the repository's settings when the run is given a GitHub client. It runs offline too, unlike a {@link GitHubCheckFunction}.
168
189
  */
169
- type SettingsAwareCheckFunction<Options> = (context: SettingsAwareCheckContext<Options>) => Promise<readonly Violation[]>;
190
+ type SettingsAwareCheckFunction<Options> = (context: SettingsAwareCheckContext<Options>) => Promise<readonly FileViolation[]>;
170
191
  //#endregion
171
192
  //#region src/migrations/adapters.d.ts
172
193
  /**
@@ -1058,7 +1079,7 @@ interface ImportCheckSpec {
1058
1079
  /**
1059
1080
  * The violations for the findings of this check's rules, in a stable order.
1060
1081
  */
1061
- readonly report: (findings: readonly IViolation[], scope: ImportScope) => readonly Violation[];
1082
+ readonly report: (findings: readonly IViolation[], scope: ImportScope) => readonly FileViolation[];
1062
1083
  }
1063
1084
  //#endregion
1064
1085
  //#region src/checks/import-cross-slice.d.ts
@@ -1179,7 +1200,7 @@ export declare const workflowJobOrdering: CheckFunction<WorkflowJobOrderingOptio
1179
1200
  *
1180
1201
  * A workflow is taken to produce required checks when it is triggered by `pull_request`; `pull_request_target` is left out, since it runs trusted code against untrusted input and is not a place for required checks. The file cannot show which checks are required, so a `pull_request` workflow whose checks are not required is reported too: narrow the check with `workflows` and `exclude`. A `merge_group` trigger on a workflow with no `pull_request` trigger is not judged.
1181
1202
  *
1182
- * Whether the branch has a merge queue is a setting, not something a file shows. Given a GitHub client, the check reads the rules of the branch (`branch`, else the default branch, of `repository`, else the `origin` remote) once it has found a workflow without the trigger, and reports only when a `merge_queue` rule applies, since without a queue no `merge_group` event is ever sent. Offline it cannot tell, so it reports every such workflow.
1203
+ * Whether the branch has a merge queue is a setting, not something a file shows. Given a GitHub client, the check reads the rules of the branch (`branch`, else the default branch, of `repository`, else the `origin` remote) once it has found a workflow without the trigger, and reports only when a `merge_queue` rule applies, since without a queue no `merge_group` event is ever sent. A repository whose plan has no rulesets (the client rejects with `RulesetsUnavailableError`) has no merge queue either, since GitHub offers merge queues only in public repositories of an organisation and in private ones on GitHub Enterprise Cloud, both of which have rulesets, so nothing is reported for it; any other refusal fails the run. Offline it cannot tell, so it reports every such workflow.
1183
1204
  *
1184
1205
  * Limits: a `merge_group` trigger takes only `branches` filters (`paths` is a filter of `push`, `pull_request` and `pull_request_target`), so a workflow path-filtered for pull requests still runs in the queue once it has the trigger; the `branches` filter of a `merge_group` trigger is not read; only rulesets are read, so a queue configured through classic branch protection is not seen and, with a client, nothing is reported for it.
1185
1206
  */
@@ -1469,4 +1490,4 @@ export declare function proveExhaustive(input: {
1469
1490
  readonly compilerOptions?: CompilerOptions;
1470
1491
  }): ExhaustivenessProof;
1471
1492
  //#endregion
1472
- export type { AggregateMappersOptions, BranchRules, CaseRemoval, CheckContext, CheckFunction, CheckName, CheckOptionsByName, CheckResult, ChecksConfig, CodecPairsOptions, CommandTypesOptions, CommitTypesOptions, ConformanceConfig, DeployDirectory, DeploySelector, DeployToolAdapter, DeployToolName, DerivedTypesOptions, DerivedTypesPair, DiscoveredPackage, DockerfilePackageManagerOptions, EnginesFloorOptions, EslintOptions, EslintRequiredSeverity, EslintSample, ExhaustivenessProof, GeneratorAdapter, GeneratorName, GitHubCheckContext, GitHubCheckFunction, GitHubClient, GitHubClientOptions, ImportGraphOptions, InstructionSymlinksOptions, Job, LayoutCheckContext, LayoutCheckFunction, MigrationsDirectoryOptions, PermissionAccess, Permissions, PinningLevel, RegisteredCheck, RepositorySettings, RepositorySlug, RunChecksOptions, RunResult, RunsOn, SettingsAwareCheckContext, SettingsAwareCheckFunction, SettingsMergeMethodsOptions, SettingsOptions, SettingsRequiredChecksOptions, SettingsReviewThreadResolutionOptions, SingleStorybookOptions, SourceLocation, Step, TypeErrorReport, UsesReference, Violation, Workflow, WorkflowActionPinningOptions, WorkflowCredentialsOptions, WorkflowJobOrderingOptions, WorkflowMergeGroupOptions, WorkflowReleaseJobOptions, WorkflowRepositoryDispatchOptions, WorkflowRunnerResolutionOptions, WorkflowSelection, WorkflowSkippableJobsOptions, WorkflowUpdateBotCooldownOptions, WorkflowVersionSingleSourceOptions, WorkspacePackage };
1493
+ export type { AggregateMappersOptions, BranchRules, CaseRemoval, CheckContext, CheckFunction, CheckName, CheckOptionsByName, CheckResult, ChecksConfig, CodecPairsOptions, CommandTypesOptions, CommitTypesOptions, ConformanceConfig, DeployDirectory, DeploySelector, DeployToolAdapter, DeployToolName, DerivedTypesOptions, DerivedTypesPair, DiscoveredPackage, DockerfilePackageManagerOptions, EnginesFloorOptions, EslintOptions, EslintRequiredSeverity, EslintSample, ExhaustivenessProof, FileViolation, GeneratorAdapter, GeneratorName, GitHubCheckContext, GitHubCheckFunction, GitHubClient, GitHubClientOptions, ImportGraphOptions, InstructionSymlinksOptions, Job, LayoutCheckContext, LayoutCheckFunction, MigrationsDirectoryOptions, PermissionAccess, Permissions, PinningLevel, RegisteredCheck, RepositorySettings, RepositorySlug, RepositoryViolation, RunChecksOptions, RunResult, RunsOn, SettingsAwareCheckContext, SettingsAwareCheckFunction, SettingsMergeMethodsOptions, SettingsOptions, SettingsRequiredChecksOptions, SettingsReviewThreadResolutionOptions, SingleStorybookOptions, SourceLocation, Step, TypeErrorReport, UsesReference, Violation, Workflow, WorkflowActionPinningOptions, WorkflowCredentialsOptions, WorkflowJobOrderingOptions, WorkflowMergeGroupOptions, WorkflowReleaseJobOptions, WorkflowRepositoryDispatchOptions, WorkflowRunnerResolutionOptions, WorkflowSelection, WorkflowSkippableJobsOptions, WorkflowUpdateBotCooldownOptions, WorkflowVersionSingleSourceOptions, WorkspacePackage };
package/dist/index.js CHANGED
@@ -2319,9 +2319,9 @@ async function branchOf(github, slug, options) {
2319
2319
  return options.branch ?? (await github.repository(slug)).defaultBranch;
2320
2320
  }
2321
2321
  /**
2322
- * What a violation of a settings check names as its file.
2322
+ * The repository a violation of a settings check names, `owner/name`.
2323
2323
  */
2324
- function repositoryFile(slug) {
2324
+ function repositoryName(slug) {
2325
2325
  return `${slug.owner}/${slug.name}`;
2326
2326
  }
2327
2327
  /**
@@ -2339,8 +2339,8 @@ async function branchRulesOf(github, slug, options) {
2339
2339
  /**
2340
2340
  * The message of a `rules-unavailable` violation: the branch rules of the repository cannot be read or enforced on its plan, so `what` cannot be required.
2341
2341
  */
2342
- function rulesUnavailableMessage(file, what, githubMessage) {
2343
- return `the branch rules of ${file} cannot be read or enforced on its plan, so ${what} cannot be required (GitHub: ${githubMessage})`;
2342
+ function rulesUnavailableMessage(repository, what, githubMessage) {
2343
+ return `the branch rules of ${repository} cannot be read or enforced on its plan, so ${what} cannot be required (GitHub: ${githubMessage})`;
2344
2344
  }
2345
2345
  //#endregion
2346
2346
  //#region src/checks/settings-merge-methods.ts
@@ -2366,7 +2366,7 @@ const METHOD_NAMES = {
2366
2366
  */
2367
2367
  const settingsMergeMethods = async ({ cwd, options, github }) => {
2368
2368
  const slug = await repositoryOf(cwd, options);
2369
- const file = repositoryFile(slug);
2369
+ const repository = repositoryName(slug);
2370
2370
  const settings = await github.repository(slug);
2371
2371
  const read = await branchRulesOf(github, slug, options);
2372
2372
  const pullRequests = "unavailable" in read ? [] : read.rules.pullRequests;
@@ -2381,13 +2381,13 @@ const settingsMergeMethods = async ({ cwd, options, github }) => {
2381
2381
  const available = METHODS.filter((method) => repositoryAllows[method] && rulesAllow(method));
2382
2382
  const violations = available.filter((method) => method !== wanted).map((method) => ({
2383
2383
  code: "settings-merge-methods/method-enabled",
2384
- message: `${file} allows ${METHOD_NAMES[method]}; only ${METHOD_NAMES[wanted]} should be allowed`,
2385
- file
2384
+ message: `${repository} allows ${METHOD_NAMES[method]}; only ${METHOD_NAMES[wanted]} should be allowed`,
2385
+ repository
2386
2386
  }));
2387
2387
  if (!available.includes(wanted)) violations.push({
2388
2388
  code: "settings-merge-methods/method-unavailable",
2389
- message: `${file} does not allow ${METHOD_NAMES[wanted]}`,
2390
- file
2389
+ message: `${repository} does not allow ${METHOD_NAMES[wanted]}`,
2390
+ repository
2391
2391
  });
2392
2392
  return violations;
2393
2393
  };
@@ -2785,12 +2785,12 @@ const REGISTRY_TOKEN_VARIABLES = ["NODE_AUTH_TOKEN", "NPM_TOKEN"];
2785
2785
  */
2786
2786
  const settingsRequiredChecks = async ({ cwd, options, github }) => {
2787
2787
  const slug = await repositoryOf(cwd, options);
2788
- const file = repositoryFile(slug);
2788
+ const repository = repositoryName(slug);
2789
2789
  const read = await branchRulesOf(github, slug, options);
2790
2790
  if ("unavailable" in read) return [{
2791
2791
  code: "settings-required-checks/rules-unavailable",
2792
- message: rulesUnavailableMessage(file, "a status check", read.unavailable),
2793
- file
2792
+ message: rulesUnavailableMessage(repository, "a status check", read.unavailable),
2793
+ repository
2794
2794
  }];
2795
2795
  const { rules } = read;
2796
2796
  const junctions = (await loadWorkflows(cwd, options)).flatMap((workflow) => jobsNamed(workflow, options.junctionJobs ?? DEFAULT_JUNCTION_JOBS).map((job) => ({
@@ -2801,26 +2801,26 @@ const settingsRequiredChecks = async ({ cwd, options, github }) => {
2801
2801
  if (junctions.length === 0) violations.push({
2802
2802
  code: "settings-required-checks/no-junction-job",
2803
2803
  message: "no workflow has a junction job, so there is no check that reports for the whole workflow to require",
2804
- file
2804
+ repository
2805
2805
  });
2806
2806
  if (rules.requiredStatusChecks.length === 0) {
2807
2807
  violations.push({
2808
2808
  code: "settings-required-checks/no-required-checks",
2809
- message: `no rule requires status checks on the branch of ${file}`,
2810
- file
2809
+ message: `no rule requires status checks on the branch of ${repository}`,
2810
+ repository
2811
2811
  });
2812
2812
  return violations;
2813
2813
  }
2814
2814
  const required = new Set(rules.requiredStatusChecks.flatMap((rule) => rule.contexts));
2815
2815
  for (const { workflow, name } of junctions.filter((junction) => !required.has(junction.name))) violations.push({
2816
2816
  code: "settings-required-checks/required-check-missing",
2817
- message: `${file} does not require the check '${name}' of ${workflow.file}`,
2818
- file
2817
+ message: `${repository} does not require the check '${name}' of ${workflow.file}`,
2818
+ repository
2819
2819
  });
2820
2820
  if (!rules.requiredStatusChecks.some((rule) => rule.strict)) violations.push({
2821
2821
  code: "settings-required-checks/not-strict",
2822
- message: `${file} does not require branches to be up to date before merging`,
2823
- file
2822
+ message: `${repository} does not require branches to be up to date before merging`,
2823
+ repository
2824
2824
  });
2825
2825
  return violations;
2826
2826
  };
@@ -2836,16 +2836,16 @@ const settingsRequiredChecks = async ({ cwd, options, github }) => {
2836
2836
  const settingsReviewThreadResolution = async ({ cwd, options, github }) => {
2837
2837
  const slug = await repositoryOf(cwd, options);
2838
2838
  const read = await branchRulesOf(github, slug, options);
2839
- const file = repositoryFile(slug);
2839
+ const repository = repositoryName(slug);
2840
2840
  if ("unavailable" in read) return [{
2841
2841
  code: "settings-review-thread-resolution/rules-unavailable",
2842
- message: rulesUnavailableMessage(file, "resolving review conversations", read.unavailable),
2843
- file
2842
+ message: rulesUnavailableMessage(repository, "resolving review conversations", read.unavailable),
2843
+ repository
2844
2844
  }];
2845
2845
  return read.rules.pullRequests.some((rule) => rule.requiredReviewThreadResolution) ? [] : [{
2846
2846
  code: "settings-review-thread-resolution/not-required",
2847
- message: `no rule requires review conversations to be resolved before merging on the branch of ${file}`,
2848
- file
2847
+ message: `no rule requires review conversations to be resolved before merging on the branch of ${repository}`,
2848
+ repository
2849
2849
  }];
2850
2850
  };
2851
2851
  //#endregion
@@ -3288,7 +3288,7 @@ const workflowJobOrdering = async ({ cwd, options }) => (await loadWorkflows(cwd
3288
3288
  *
3289
3289
  * A workflow is taken to produce required checks when it is triggered by `pull_request`; `pull_request_target` is left out, since it runs trusted code against untrusted input and is not a place for required checks. The file cannot show which checks are required, so a `pull_request` workflow whose checks are not required is reported too: narrow the check with `workflows` and `exclude`. A `merge_group` trigger on a workflow with no `pull_request` trigger is not judged.
3290
3290
  *
3291
- * Whether the branch has a merge queue is a setting, not something a file shows. Given a GitHub client, the check reads the rules of the branch (`branch`, else the default branch, of `repository`, else the `origin` remote) once it has found a workflow without the trigger, and reports only when a `merge_queue` rule applies, since without a queue no `merge_group` event is ever sent. Offline it cannot tell, so it reports every such workflow.
3291
+ * Whether the branch has a merge queue is a setting, not something a file shows. Given a GitHub client, the check reads the rules of the branch (`branch`, else the default branch, of `repository`, else the `origin` remote) once it has found a workflow without the trigger, and reports only when a `merge_queue` rule applies, since without a queue no `merge_group` event is ever sent. A repository whose plan has no rulesets (the client rejects with `RulesetsUnavailableError`) has no merge queue either, since GitHub offers merge queues only in public repositories of an organisation and in private ones on GitHub Enterprise Cloud, both of which have rulesets, so nothing is reported for it; any other refusal fails the run. Offline it cannot tell, so it reports every such workflow.
3292
3292
  *
3293
3293
  * Limits: a `merge_group` trigger takes only `branches` filters (`paths` is a filter of `push`, `pull_request` and `pull_request_target`), so a workflow path-filtered for pull requests still runs in the queue once it has the trigger; the `branches` filter of a `merge_group` trigger is not read; only rulesets are read, so a queue configured through classic branch protection is not seen and, with a client, nothing is reported for it.
3294
3294
  */
@@ -3298,8 +3298,8 @@ const workflowMergeGroup = async ({ cwd, options, github }) => {
3298
3298
  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)];
3299
3299
  });
3300
3300
  if (violations.length === 0 || github === void 0) return violations;
3301
- const slug = await repositoryOf(cwd, options);
3302
- return (await github.branchRules(slug, await branchOf(github, slug, options))).mergeQueue ? violations : [];
3301
+ const branch = await branchRulesOf(github, await repositoryOf(cwd, options), options);
3302
+ return "rules" in branch && branch.rules.mergeQueue ? violations : [];
3303
3303
  };
3304
3304
  //#endregion
3305
3305
  //#region src/checks/workflow-release-job.ts
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": "3.0.0",
4
+ "version": "4.0.1",
5
5
  "bin": {
6
6
  "workspace-conformance": "./dist/cli.js"
7
7
  },