@avi2dg/checks 0.31.0 → 0.33.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/CHANGELOG.md +24 -0
- package/README.md +6 -3
- package/dist/readability/index.js +18 -9
- package/docs/configs/commit-messages.md +5 -0
- package/docs/configs/typescript-rules.md +1 -1
- package/docs/design.md +42 -4
- package/docs/gates/checks-commit-identity.md +3 -0
- package/docs/gates/checks-docs.md +9 -3
- package/docs/gates/checks-lint.md +1 -1
- package/docs/gates/checks-mutation.md +2 -1
- package/docs/gates/checks-release-notes.md +10 -1
- package/docs/gates/checks-release-pr.md +185 -0
- package/docs/gates/checks-release-report.md +4 -2
- package/docs/gates/checks-release-tag.md +63 -0
- package/docs/gates/checks-secrets.md +93 -0
- package/docs/gates/checks-vendor.md +3 -3
- package/package.json +23 -6
- package/src/core/gates.ts +1 -0
- package/src/core/git.ts +1 -1
- package/src/core/lint.ts +1 -1
- package/src/delivery/commit-identity.ts +17 -1
- package/src/delivery/github.ts +38 -0
- package/src/delivery/gitleaks.toml +113 -0
- package/src/delivery/gitleaks.ts +111 -0
- package/src/delivery/release-pr.ts +196 -0
- package/src/delivery/release-report.ts +3 -30
- package/src/delivery/release-tag.ts +64 -0
- package/src/delivery/release.ts +79 -0
- package/src/delivery/secrets.ts +86 -0
- package/src/dependencies/osv-scanner.ts +3 -45
- package/src/dependencies/pinned-binary.ts +80 -0
- package/src/docs/doc-agents.ts +22 -20
- package/src/docs/doc-names.ts +1 -1
- package/src/docs/doc-references.ts +14 -7
- package/src/docs/docs.ts +15 -2
- package/src/docs/prose-matchers.ts +1 -1
- package/src/docs/sentence-length.ts +106 -0
- package/src/testing/flake.ts +1 -1
- package/src/testing/mutation.ts +1 -1
- package/src/testing/test.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
|
|
4
4
|
|
|
5
|
+
## 0.33.0
|
|
6
|
+
|
|
7
|
+
Released 2026-10-03.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **docs:** report sentences over trial length caps as advisory [#120](https://github.com/avi2d/checks/pull/120)
|
|
12
|
+
|
|
13
|
+
### Fixes
|
|
14
|
+
|
|
15
|
+
- move effect to stable 4.0.0 [#122](https://github.com/avi2d/checks/pull/122)
|
|
16
|
+
- **complexity:** accept signed numbers and earlier props names in thin-astro defaults [#115](https://github.com/avi2d/checks/pull/115)
|
|
17
|
+
- **docs:** judge agent file entries and headings from a CommonMark parse [#116](https://github.com/avi2d/checks/pull/116)
|
|
18
|
+
- **docs:** fail link anchors the checks-docs reference rule could not check [#114](https://github.com/avi2d/checks/pull/114)
|
|
19
|
+
|
|
20
|
+
## 0.32.0
|
|
21
|
+
|
|
22
|
+
Released 2026-09-29.
|
|
23
|
+
|
|
24
|
+
### Features
|
|
25
|
+
|
|
26
|
+
- **delivery:** add checks-secrets gate that fails a range whose commits add a secret [#112](https://github.com/avi2d/checks/pull/112)
|
|
27
|
+
- **delivery:** open a release pull request daily and tag it when it merges [#111](https://github.com/avi2d/checks/pull/111)
|
|
28
|
+
|
|
5
29
|
## 0.31.0
|
|
6
30
|
|
|
7
31
|
Released 2026-09-28.
|
package/README.md
CHANGED
|
@@ -9,12 +9,12 @@ Each repository owns its workflows and native tool configs, as [Native settings]
|
|
|
9
9
|
<!-- generated prerequisites: bun run build writes it from package.json, .bun-version and scripts/doc-blocks.ts -->
|
|
10
10
|
|
|
11
11
|
- A git repository, whose history the range gates read.
|
|
12
|
-
- Bun 1.
|
|
12
|
+
- Bun 1.4.2, which runs every bin.
|
|
13
13
|
- The peer dependencies, at the exact versions the kit pins:
|
|
14
14
|
- `@effect/tsgo` 0.45.0
|
|
15
15
|
- `@swc/core` 1.16.2
|
|
16
16
|
- `dependency-cruiser` 18.4.0
|
|
17
|
-
- `effect` 4.0.0
|
|
17
|
+
- `effect` 4.0.0
|
|
18
18
|
- `jscpd` 5.3.2
|
|
19
19
|
- `oxlint` 1.83.0
|
|
20
20
|
- `oxlint-tsgolint` 7.0.2002
|
|
@@ -31,7 +31,7 @@ To consume the kit from a repository:
|
|
|
31
31
|
<!-- generated install: bun run build writes it from package.json and scripts/doc-blocks.ts -->
|
|
32
32
|
|
|
33
33
|
```sh
|
|
34
|
-
bun add -d @avi2dg/checks @effect/tsgo@0.45.0 @swc/core@1.16.2 dependency-cruiser@18.4.0 effect@4.0.0
|
|
34
|
+
bun add -d @avi2dg/checks @effect/tsgo@0.45.0 @swc/core@1.16.2 dependency-cruiser@18.4.0 effect@4.0.0 jscpd@5.3.2 oxlint@1.83.0 oxlint-tsgolint@7.0.2002 typescript@7.0.2
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
<!-- end generated install -->
|
|
@@ -127,6 +127,7 @@ The table groups the gates by vector, the part of a repository each one judges.
|
|
|
127
127
|
| docs | [`checks-docs`](docs/gates/checks-docs.md) | the range, and every agent file at the head commit | every repository |
|
|
128
128
|
| delivery | [`checks-commit-identity`](docs/gates/checks-commit-identity.md) | the range | every repository |
|
|
129
129
|
| delivery | [`checks-ci-wiring`](docs/gates/checks-ci-wiring.md) | the working tree | every repository |
|
|
130
|
+
| delivery | [`checks-secrets`](docs/gates/checks-secrets.md) | the range | every repository |
|
|
130
131
|
| dependencies | [`checks-advisories`](docs/gates/checks-advisories.md) | the range | a repository tracking `bun.lock` |
|
|
131
132
|
|
|
132
133
|
<!-- end generated gates -->
|
|
@@ -141,6 +142,8 @@ These bins run on their own:
|
|
|
141
142
|
- [`checks-changelog`](docs/gates/checks-changelog.md) writes the pending release into `CHANGELOG.md` from the conventional commits since the last release.
|
|
142
143
|
- [`checks-release-notes`](docs/gates/checks-release-notes.md) writes one `CHANGELOG.md` section to a file for a GitHub release.
|
|
143
144
|
- [`checks-release-report`](docs/gates/checks-release-report.md) tells whether the history holds unreleased features or fixes since the last tag.
|
|
145
|
+
- [`checks-release-pr`](docs/gates/checks-release-pr.md) opens or refreshes the pull request that releases the next version, and dispatches its checks.
|
|
146
|
+
- [`checks-release-tag`](docs/gates/checks-release-tag.md) tags a landed release commit with its version and dispatches the release workflow on the tag.
|
|
144
147
|
- [`checks-vendor`](docs/gates/checks-vendor.md) pins each library its `prepare` arguments name to a shared read-only clone and links it under `repos/`.
|
|
145
148
|
|
|
146
149
|
`checks-lint` has [its own page](docs/gates/checks-lint.md), which says which range it resolves.
|
|
@@ -400,11 +400,11 @@ var rule = {
|
|
|
400
400
|
create(context) {
|
|
401
401
|
const max = maxOf(context.options);
|
|
402
402
|
const check = (node) => {
|
|
403
|
-
const
|
|
404
|
-
if (
|
|
403
|
+
const score = node.type === "StaticBlock" ? cognitiveComplexity(node, NO_NAMES) : cognitiveComplexity(node, selfNames(node));
|
|
404
|
+
if (score <= max)
|
|
405
405
|
return;
|
|
406
406
|
const name = node.type === "StaticBlock" ? "static block" : `function \`${displayName(node)}\``;
|
|
407
|
-
context.report({ node, message: `${name} has a cognitive complexity of ${
|
|
407
|
+
context.report({ node, message: `${name} has a cognitive complexity of ${score}. Maximum allowed is ${max}.` });
|
|
408
408
|
};
|
|
409
409
|
return {
|
|
410
410
|
FunctionDeclaration: check,
|
|
@@ -430,6 +430,9 @@ function isAstroProps(value) {
|
|
|
430
430
|
const { object, property } = value;
|
|
431
431
|
return object.type === "Identifier" && object.name === "Astro" && property.type === "Identifier" && property.name === "props";
|
|
432
432
|
}
|
|
433
|
+
function isSignedNumber(value) {
|
|
434
|
+
return value.type === "UnaryExpression" && (value.operator === "-" || value.operator === "+") && value.argument.type === "Literal" && typeof value.argument.value === "number";
|
|
435
|
+
}
|
|
433
436
|
function isPlainValue(value, bound) {
|
|
434
437
|
return value.type === "Literal" || value.type === "Identifier" && bound.has(value.name);
|
|
435
438
|
}
|
|
@@ -442,17 +445,23 @@ function readsProps(expression, bound) {
|
|
|
442
445
|
return isAstroProps(value) || readsProps(value.object, bound);
|
|
443
446
|
}
|
|
444
447
|
function isPlainPattern(pattern, bound) {
|
|
448
|
+
return isPlainInOrder(pattern, bound, new Set(bound));
|
|
449
|
+
}
|
|
450
|
+
function isPlainInOrder(pattern, bound, seen) {
|
|
445
451
|
if (pattern === null)
|
|
446
452
|
return true;
|
|
447
453
|
if (pattern.type === "RestElement")
|
|
448
|
-
return
|
|
449
|
-
if (pattern.type === "AssignmentPattern")
|
|
450
|
-
return isPlainValue(pattern.right,
|
|
454
|
+
return isPlainInOrder(pattern.argument, bound, seen);
|
|
455
|
+
if (pattern.type === "AssignmentPattern") {
|
|
456
|
+
return (isPlainValue(pattern.right, seen) || isSignedNumber(pattern.right)) && isPlainInOrder(pattern.left, bound, seen);
|
|
457
|
+
}
|
|
451
458
|
if (pattern.type === "ArrayPattern")
|
|
452
|
-
return pattern.elements.every((element) =>
|
|
453
|
-
if (pattern.type === "Identifier")
|
|
459
|
+
return pattern.elements.every((element) => isPlainInOrder(element, bound, seen));
|
|
460
|
+
if (pattern.type === "Identifier") {
|
|
461
|
+
seen.add(pattern.name);
|
|
454
462
|
return true;
|
|
455
|
-
|
|
463
|
+
}
|
|
464
|
+
return pattern.properties.every((property) => property.type === "RestElement" ? isPlainInOrder(property.argument, bound, seen) : (!property.computed || isPlainValue(property.key, bound)) && isPlainInOrder(property.value, bound, seen));
|
|
456
465
|
}
|
|
457
466
|
function isPropsRead(declarator, bound) {
|
|
458
467
|
return declarator.init !== null && readsProps(declarator.init, bound) && isPlainPattern(declarator.id, bound);
|
|
@@ -23,6 +23,11 @@ It lints the pull request title and nothing else.
|
|
|
23
23
|
The title is the enforced subject because a squash merge uses it as the main commit subject, and per-commit messages are not linted.
|
|
24
24
|
GitHub appends ` (#N)` to the squashed subject, so the workflow lints the title with that suffix attached, and the header length limit applies to the landed subject, not the bare title.
|
|
25
25
|
The workflow moves git's comment character off `#`, so a title starting with `#` is linted like any other.
|
|
26
|
+
The workflow also triggers on `workflow_dispatch`, since GitHub holds the `pull_request` runs of a release pull request the workflow token opens until a maintainer approves them, as [checks-release-pr](../gates/checks-release-pr.md) says.
|
|
27
|
+
A dispatched run carries no pull request, so the workflow reads the title of the one open pull request its branch heads, and fails when there is none.
|
|
28
|
+
That lookup needs the workflow's `pull-requests: read` permission.
|
|
29
|
+
It reports a dispatched run's result on the head commit as a `commitlint` status, since GitHub keeps a dispatched run's check off the pull request.
|
|
30
|
+
The kit's own `.github/workflows/commitlint.yml` shows the step.
|
|
26
31
|
|
|
27
32
|
It never sees a commit's author or committer fields, nor the `Co-authored-by` trailer GitHub writes from a foreign author when it squashes, so it cannot enforce who a commit belongs to.
|
|
28
33
|
[checks-commit-identity](../gates/checks-commit-identity.md) is that enforcement.
|
|
@@ -48,7 +48,7 @@ The base loads the kit's `data-shape` plugin from `dist/` with one rule for ever
|
|
|
48
48
|
An override in `oxlintrc.json` turns on one rule of the kit's `readability` plugin in each `.astro` file:
|
|
49
49
|
|
|
50
50
|
- `readability/thin-astro` refuses a statement in the frontmatter or a script block that is neither an import, a re-export from another module, a type or interface declaration, nor a variable read from `Astro.props`.
|
|
51
|
-
- A default inside an `Astro.props` destructuring passes only when it is a literal or a name read from `Astro.props` earlier, so `const { title = "Home" } = Astro.props;` passes and a call or `await` in a default is refused.
|
|
51
|
+
- A default inside an `Astro.props` destructuring passes only when it is a literal, a `-` or `+` on a numeric literal, or a name read from `Astro.props` earlier, in the same pattern or a statement before it, so `const { title = "Home", heading = title } = Astro.props;` passes and a call or `await` in a default is refused.
|
|
52
52
|
- Move a refused statement into a `.ts` file and import it, so the `.astro` file holds only imports, props and markup.
|
|
53
53
|
- A script block loads client code with a side-effect import, as in `<script>import "../client.ts";</script>`, and the override turns off `import/no-unassigned-import` so that import passes.
|
|
54
54
|
- A dynamic route re-exports `getStaticPaths` from a `.ts` file, as in `export { getStaticPaths } from "../lib/paths.ts";`.
|
package/docs/design.md
CHANGED
|
@@ -67,7 +67,7 @@ So the fragment lists the rules in `files`, and in `include` beside every file u
|
|
|
67
67
|
|
|
68
68
|
The source sits under `src/<vector>/`, one directory for each thing the kit judges a repository on: complexity, quality, testing, docs, delivery and dependencies.
|
|
69
69
|
`src/core/` holds what every vector runs on.
|
|
70
|
-
`scripts/` holds only the kit's own build, and nothing in it ships.
|
|
70
|
+
`scripts/` holds only the kit's own build and CI tooling, and nothing in it ships.
|
|
71
71
|
Sorting files by what loads them would put both oxlint plugins at the root and every bin in one flat directory.
|
|
72
72
|
Nothing would then say which gate a helper serves.
|
|
73
73
|
A mutation runner's default scope covers `src/`, so the kit's own Stryker run mutates its source with no `mutate` list.
|
|
@@ -100,7 +100,7 @@ The `.ts` bins keep a `bun` shebang and need no build step, unlike the oxlint pl
|
|
|
100
100
|
The bins are written in Effect.
|
|
101
101
|
So `effect` is a peer dependency, and `@effect/platform-bun`, which only the bins use, is a dependency.
|
|
102
102
|
`@effect/platform-node-shared` is a direct dependency only to pin its version.
|
|
103
|
-
`@effect/platform-bun` asks for it with a `^` range,
|
|
103
|
+
`@effect/platform-bun` asks for it with a `^` range, so a newer minor of it could resolve and peer on a newer `effect` than the exact version consumers install.
|
|
104
104
|
So the three packages move together at one exact version.
|
|
105
105
|
|
|
106
106
|
## checks-lint runs each gate as its own bin
|
|
@@ -162,8 +162,24 @@ So a checkout without tags, a fork, and a branch that merged `main` in all write
|
|
|
162
162
|
A section keeps the date it was written, because the squash merge that lands the release commit may fall on another day.
|
|
163
163
|
Entries come from commit subjects, which are the squash-merged pull request titles that commitlint holds to the conventional format.
|
|
164
164
|
A commit body holds the branch's own messages, and nothing lints it, so no entry comes from a body.
|
|
165
|
-
The changelog arrives in the release pull request, and no workflow pushes to
|
|
166
|
-
|
|
165
|
+
The changelog arrives in the release pull request, and no workflow pushes to a branch a person works on.
|
|
166
|
+
|
|
167
|
+
## A release is cut every day
|
|
168
|
+
|
|
169
|
+
A release waits for no quiet moment, since a busy repository always has work under way.
|
|
170
|
+
The daily release opens the release pull request from `main` whenever it holds a feature or a fix since the last tag, and the pull request merges through the repository's usual merge path once its checks pass.
|
|
171
|
+
`checks-release-pr` writes only to the `release/<branch>` branch it owns, and rebuilds that branch on `main` rather than merging `main` into it, so the changelog it carries is the one the build writes.
|
|
172
|
+
`checks-release-tag` writes only the `v*` tag of a release commit that already landed.
|
|
173
|
+
The `github-release` job's `contents: write` creates or updates the GitHub release from the tag's `CHANGELOG.md` section.
|
|
174
|
+
|
|
175
|
+
Both jobs act with the workflow token.
|
|
176
|
+
GitHub starts no run for a push that token makes, and holds the runs of a pull request it opens until a maintainer approves them.
|
|
177
|
+
A GitHub App or a personal token would start them, but either is a credential each repository stores and someone rotates.
|
|
178
|
+
So `checks-release-pr` dispatches the required checks on the release head, and `checks-release-tag` dispatches the release workflow on the tag, since a dispatch is the one run the token can start.
|
|
179
|
+
GitHub keeps a dispatched run's checks off the pull request, and branch protection does not count them.
|
|
180
|
+
So each dispatched job reports its result as a commit status named for the job, which a required check of that name counts.
|
|
181
|
+
Where a status and a check share a name, branch protection requires both, so the status never passes a pull request whose own check failed.
|
|
182
|
+
The cost is a `workflow_dispatch` trigger and a status step on each workflow a release needs, a title lint that reads its title from the open pull request, and the repository setting that lets the token open a pull request.
|
|
167
183
|
|
|
168
184
|
## The docs gate judges what a change touches
|
|
169
185
|
|
|
@@ -195,6 +211,10 @@ A score cannot fail a change without failing correct prose, and a suggestion tha
|
|
|
195
211
|
`src/docs/prose-matchers.ts` imports nothing, so the gate and a write-time hook run one matcher and refuse in the same words.
|
|
196
212
|
A hook bundle ships without `node_modules`, so a matcher that needed Vale or another package could not refuse at write time.
|
|
197
213
|
|
|
214
|
+
The entry rule and the rule against a `## Maintaining this file` section take their list items and headings from `commonmark`, the CommonMark reference parser.
|
|
215
|
+
A reader sees the blocks a renderer builds, and a line rule that guesses at blockquotes, HTML blocks and indented code misjudges each corner its guess misses.
|
|
216
|
+
Only the gate runs these rules, so the parser costs no hook anything, and the line scan reads each entry's code spans and links from that entry's line alone.
|
|
217
|
+
|
|
198
218
|
A path, link or command on a line the range leaves alone still fails when the range broke it, for example by deleting the file it names.
|
|
199
219
|
A reference goes stale far more often because the code it names moves than because its own line is edited.
|
|
200
220
|
So a gate on edited lines alone would miss the usual break.
|
|
@@ -238,6 +258,24 @@ The kit caps each entry at 30 days and measures a range from the head's dates, a
|
|
|
238
258
|
`--all` measures from the current time instead, because a head's dates never move in a repository that takes no commit, and an entry measured from them would never expire.
|
|
239
259
|
The file is JSON because `Bun.TOML` cannot parse a TOML date.
|
|
240
260
|
|
|
261
|
+
## Secrets fail in every commit of the range
|
|
262
|
+
|
|
263
|
+
`checks-secrets` scans each commit in the range rather than the files at the head.
|
|
264
|
+
A secret a commit adds stays in that commit after a later commit deletes it, and a pushed branch has already sent it to the forge.
|
|
265
|
+
So the fix is to rewrite the commit that added it, and to rotate the secret once it has left the machine.
|
|
266
|
+
|
|
267
|
+
In a merge commit the gate scans only the merge's own resolution, the difference from the merge git would make on its own.
|
|
268
|
+
A diff against the first parent would also scan what the merge brings in from the other parent, so a branch that merges main would fail on a secret main already holds, from before the range.
|
|
269
|
+
Git gives no such diff for an octopus merge and warns instead of failing, so the gate refuses to scan one rather than pass a commit it never read.
|
|
270
|
+
|
|
271
|
+
The gate runs gitleaks rather than a hand-written pattern list, because its default config already holds over 200 rules for the token shapes of common services.
|
|
272
|
+
The kit's own rules in `src/delivery/gitleaks.toml` add only the VPN keys and proxy links those rules miss.
|
|
273
|
+
The kit pins gitleaks by version and SHA-256 for the reason it pins OSV-Scanner.
|
|
274
|
+
|
|
275
|
+
No finding can be accepted.
|
|
276
|
+
A secret has no false positive worth keeping in git, because a placeholder carries the same shape without the value.
|
|
277
|
+
So the gate ignores a repository's `.gitleaks.toml`, `.gitleaksignore` and `gitleaks:allow` comments, which would each let one repository pass what another fails.
|
|
278
|
+
|
|
241
279
|
## Related topics
|
|
242
280
|
|
|
243
281
|
- [checks](../README.md)
|
|
@@ -11,6 +11,9 @@ audience: consumers
|
|
|
11
11
|
The author and committer of each commit must be allowed.
|
|
12
12
|
A commit whose trailer block carries a `Co-authored-by` trailer, as git parses it, is refused.
|
|
13
13
|
`GitHub <noreply@github.com>` is allowed as committer only.
|
|
14
|
+
`github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>` is allowed as author only of a commit whose subject is `chore: release <version>`, with or without the pull request number a squash merge adds.
|
|
15
|
+
GitHub attributes the release commit [checks-release-pr](checks-release-pr.md) makes to that bot, since it commits through the workflow token.
|
|
16
|
+
Such a commit may carry a `Co-authored-by` trailer naming that bot, since GitHub adds one to some squash merges of a pull request the bot opened, and every other trailer is refused.
|
|
14
17
|
|
|
15
18
|
## What it reads
|
|
16
19
|
|
|
@@ -110,6 +110,8 @@ A line holds one sentence, so a changed line is a changed sentence.
|
|
|
110
110
|
A bold label that opens a line, as in `**Status.**`, heads the sentence after it and is not a sentence of its own.
|
|
111
111
|
No rule reads fenced code, inline code, link destinations, URLs, HTML comments or front matter.
|
|
112
112
|
Readability scores and word choice, such as easy, are not checked.
|
|
113
|
+
A sentence over the trial length cap is listed as advisory and never fails the run.
|
|
114
|
+
The cap is 20 words in an ordered list item and 25 words elsewhere.
|
|
113
115
|
|
|
114
116
|
`src/docs/prose-matchers.ts` holds the rules and a synchronous `proseRefused()`, and imports nothing.
|
|
115
117
|
The package exports it as `@avi2dg/checks/scripts/prose-matchers.ts`.
|
|
@@ -121,7 +123,8 @@ Each reference a living doc or an agent file names has to resolve at the head co
|
|
|
121
123
|
|
|
122
124
|
- A path in inline code that ends in a file extension, such as `src/core/lint.ts`, names a file from the root or from the doc's directory.
|
|
123
125
|
- A relative Markdown link names a file or a directory.
|
|
124
|
-
Its anchor names a heading in that file, as GitHub derives the anchor, or an explicit `id`.
|
|
126
|
+
Its anchor names a heading in that Markdown or MDX file, as GitHub derives the anchor, or an explicit `id`.
|
|
127
|
+
An anchor into a directory or any other file fails, because it names no heading the gate can check.
|
|
125
128
|
- A `bun run` command in code names a script in the nearest `package.json`, a bin in `node_modules/.bin`, or a file that exists.
|
|
126
129
|
- A code span fails when some tracked file outside the docs held that exact text at the base, and none holds it at the head.
|
|
127
130
|
A span the path check already fails on is not reported again.
|
|
@@ -150,7 +153,8 @@ audience: consumers
|
|
|
150
153
|
An agent file holds the router its template sketches, and the rules below hold its shape whatever the range touches.
|
|
151
154
|
A file over 3,000 characters fails.
|
|
152
155
|
Move each part's notes into the people doc that covers that part, and delete what a check or the code already holds.
|
|
153
|
-
A file that holds a `## Maintaining this file` section fails, because this gate holds the shape the section asked for.
|
|
156
|
+
A file that holds a `## Maintaining this file` section a reader sees fails, because this gate holds the shape the section asked for.
|
|
157
|
+
A heading inside an HTML comment, an HTML block or a code block is not such a section.
|
|
154
158
|
Each entry names at least one of these, or it fails:
|
|
155
159
|
|
|
156
160
|
- A path in inline code that git tracks at the head commit, a file or a directory, such as `package.json`, `LICENSE` or `.gitignore`.
|
|
@@ -159,7 +163,7 @@ Each entry names at least one of these, or it fails:
|
|
|
159
163
|
- A `bun run` command.
|
|
160
164
|
|
|
161
165
|
Whether the link or the command resolves is the reference rule's call, as [Paths, links and commands](#paths-links-and-commands) says, and it fails when the range adds or breaks one.
|
|
162
|
-
An entry is any list item a reader sees, the items above the first section included.
|
|
166
|
+
An entry is any list item a reader sees as CommonMark renders the file, the items above the first section included.
|
|
163
167
|
A list item inside an HTML comment, an HTML block or an indented code block is not an entry.
|
|
164
168
|
A fresh file passes the ceiling, and its entries pass once each names the file that holds its detail.
|
|
165
169
|
|
|
@@ -205,6 +209,8 @@ docs: 6 violation(s):
|
|
|
205
209
|
docs/parts.md:9: names `gates.lint`, which the range removed from every file outside the docs. Say what holds now, or drop the line
|
|
206
210
|
docs: advisory, 1 doc file(s) the range leaves alone do not hold to their templates yet:
|
|
207
211
|
docs/adr/0001-quality-gates.md: 5 violation(s)
|
|
212
|
+
docs: advisory, 1 sentence(s) over the trial length caps:
|
|
213
|
+
README.md:16: carries a 27-word descriptive sentence, over the 25-word cap (procedural means an ordered list item, capped at 20 words)
|
|
208
214
|
docs: advisory, 1 path(s), link(s) or command(s) the living docs or agent files name were broken before the range:
|
|
209
215
|
docs/parts.md:9: links to `suppliers.md#prices`, and `docs/suppliers.md` has no heading with that anchor
|
|
210
216
|
```
|
|
@@ -46,7 +46,7 @@ With two arguments, the base and head override range discovery.
|
|
|
46
46
|
|
|
47
47
|
```
|
|
48
48
|
checks-lint: range 2504acf098d120e73a8ece3c96f22b934f35c6a8..10ba7d8935b73ed72624120a1542e51bd21ca7c7 from HEAD against origin/main
|
|
49
|
-
checks-lint: 1 of
|
|
49
|
+
checks-lint: 1 of 13 gate(s) failed: checks-comment-gate
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
<!-- end generated lint-sample -->
|
|
@@ -59,8 +59,9 @@ An `--incremental` run with no report stops before instrumenting with the missin
|
|
|
59
59
|
## When it runs
|
|
60
60
|
|
|
61
61
|
A repository runs full baselines from the mutation workflow on `workflow_dispatch`.
|
|
62
|
+
A repository that picks a pull request scope from a baseline also runs that workflow on a nightly schedule.
|
|
62
63
|
Run scoped checks locally during development.
|
|
63
|
-
A scheduled run
|
|
64
|
+
A scheduled run costs a full Stryker run.
|
|
64
65
|
|
|
65
66
|
## Running it in CI
|
|
66
67
|
|
|
@@ -55,6 +55,7 @@ name: release
|
|
|
55
55
|
on:
|
|
56
56
|
push:
|
|
57
57
|
tags: ["v*"]
|
|
58
|
+
workflow_dispatch:
|
|
58
59
|
permissions:
|
|
59
60
|
contents: read
|
|
60
61
|
id-token: write
|
|
@@ -65,6 +66,8 @@ jobs:
|
|
|
65
66
|
- uses: actions/checkout@v5
|
|
66
67
|
with:
|
|
67
68
|
fetch-depth: 0
|
|
69
|
+
- name: ref is a tag
|
|
70
|
+
run: test "$GITHUB_REF_TYPE" = tag
|
|
68
71
|
- uses: oven-sh/setup-bun@v2
|
|
69
72
|
- run: bun install --frozen-lockfile
|
|
70
73
|
- run: bun run build
|
|
@@ -102,6 +105,7 @@ name: release
|
|
|
102
105
|
on:
|
|
103
106
|
push:
|
|
104
107
|
tags: ["v*"]
|
|
108
|
+
workflow_dispatch:
|
|
105
109
|
permissions:
|
|
106
110
|
contents: read
|
|
107
111
|
jobs:
|
|
@@ -113,6 +117,8 @@ jobs:
|
|
|
113
117
|
- uses: actions/checkout@v5
|
|
114
118
|
with:
|
|
115
119
|
fetch-depth: 0
|
|
120
|
+
- name: ref is a tag
|
|
121
|
+
run: test "$GITHUB_REF_TYPE" = tag
|
|
116
122
|
- uses: oven-sh/setup-bun@v2
|
|
117
123
|
- run: bun install --frozen-lockfile
|
|
118
124
|
- run: bun run build
|
|
@@ -127,7 +133,10 @@ jobs:
|
|
|
127
133
|
run: gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --notes-file "$RUNNER_TEMP/release-notes.md"
|
|
128
134
|
```
|
|
129
135
|
|
|
130
|
-
|
|
136
|
+
A release is a pull request that holds only the version bump and the built changelog, which [checks-release-pr](checks-release-pr.md) opens once a day.
|
|
137
|
+
When it lands, [checks-release-tag](checks-release-tag.md) tags the merge commit and dispatches this workflow on the tag.
|
|
138
|
+
A tag the workflow token pushes starts no `push` run, so the workflow triggers on `workflow_dispatch` as well, and refuses a dispatch on a ref that is not a tag.
|
|
139
|
+
A tag a person pushes starts the same workflow through its `push` trigger.
|
|
131
140
|
The workflow refuses a tag that disagrees with `package.json`, so the tag always names the section the notes come from.
|
|
132
141
|
|
|
133
142
|
## When it runs
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: reference
|
|
3
|
+
audience: consumers
|
|
4
|
+
---
|
|
5
|
+
# checks-release-pr
|
|
6
|
+
|
|
7
|
+
`checks-release-pr` opens or refreshes the one pull request that releases the next version, and dispatches its checks on its head.
|
|
8
|
+
|
|
9
|
+
## What it checks
|
|
10
|
+
|
|
11
|
+
It lists the unreleased changes the way [checks-release-report](checks-release-report.md) does, and does nothing when there are none.
|
|
12
|
+
It bumps the version `package.json` holds by the kit's rule:
|
|
13
|
+
|
|
14
|
+
| Unreleased changes | Below 1.0.0 | From 1.0.0 |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| a breaking change | minor | major |
|
|
17
|
+
| a feature and no breaking change | minor | minor |
|
|
18
|
+
| only fixes, performance changes or reverts | patch | patch |
|
|
19
|
+
|
|
20
|
+
It refuses when the last release tag names a version other than the one `package.json` holds, since an untagged bump means a release landed that nothing published.
|
|
21
|
+
The refusal points at the `tag` job of `daily-release` on the release commit, whose own refusal says what to fix, rather than at a tag pushed by hand.
|
|
22
|
+
It refuses a version that is not a plain `major.minor.patch`.
|
|
23
|
+
It writes the next version into `package.json`, runs `bun run build` so the build writes `CHANGELOG.md`, and commits every tracked file the build changed as `chore: release <version>`.
|
|
24
|
+
The commit's one parent is `HEAD`.
|
|
25
|
+
It makes the commit through the GitHub API, which attributes it to `github-actions[bot]` and signs it, so the job sets no git identity.
|
|
26
|
+
It checks that the tree GitHub built matches the tree the build wrote.
|
|
27
|
+
It points the branch `release/<branch>` at the commit, where `<branch>` is the branch `HEAD` is on.
|
|
28
|
+
It opens a pull request from that branch into `<branch>` titled `chore: release <version>`, with the body `Release <version>.`, or retitles the open one to the new version.
|
|
29
|
+
It then dispatches each workflow its arguments name on the release branch.
|
|
30
|
+
GitHub holds the `pull_request` runs of a pull request the workflow token opens until a maintainer approves them, so the dispatch is what runs the required checks on the release head.
|
|
31
|
+
GitHub keeps a dispatched run's checks off the pull request, so each dispatched job reports its result as a commit status named for the job, which the required check of that name counts.
|
|
32
|
+
When the release branch already holds this version on top of `HEAD` and its pull request carries the right title, it pushes nothing and dispatches nothing.
|
|
33
|
+
It leaves the working tree as it found it.
|
|
34
|
+
|
|
35
|
+
## What it reads
|
|
36
|
+
|
|
37
|
+
It reads the `v*` tags, the commit subjects since the last one and `package.json` from the checkout.
|
|
38
|
+
It refuses a shallow checkout, a detached `HEAD` and a working tree with changes to tracked files.
|
|
39
|
+
It reads the release branch from `origin` with `git ls-remote` and `git fetch`.
|
|
40
|
+
It calls the GitHub API through `gh api`, which takes the repository from the checkout's remote and the token from `GH_TOKEN`.
|
|
41
|
+
The token needs `contents: write`, `pull-requests: write` and `actions: write`.
|
|
42
|
+
The repository needs **Allow GitHub Actions to create and approve pull requests** turned on under its Actions settings, or GitHub refuses the pull request.
|
|
43
|
+
|
|
44
|
+
## Arguments
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
checks-release-pr <workflow>...
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Each argument names a workflow file under `.github/workflows/` whose jobs report the checks the default branch requires, such as `ci.yml` and `commitlint.yml`.
|
|
51
|
+
Each named workflow triggers on `workflow_dispatch`.
|
|
52
|
+
Name no workflow that runs something else on dispatch, such as a mutation baseline.
|
|
53
|
+
|
|
54
|
+
## Exit codes
|
|
55
|
+
|
|
56
|
+
| Code | When |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| 0 | nothing is unreleased, or the release pull request is open on the current `HEAD` and its checks are dispatched |
|
|
59
|
+
| 2 | the arguments do not parse, a refusal above applies, the build fails, or a GitHub API call fails |
|
|
60
|
+
|
|
61
|
+
## Sample output
|
|
62
|
+
|
|
63
|
+
A run that opens the pull request prints the build's own output, then one line:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
release-pr: opened https://github.com/acme/widget/pull/12 to release 0.4.0, and dispatched ci.yml, commitlint.yml on release/main
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A run on the same `HEAD` the next day prints one line:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
release-pr: https://github.com/acme/widget/pull/12 releases 0.4.0 from 3f2a9c81d0b4 and is current
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## When it runs
|
|
76
|
+
|
|
77
|
+
The daily release workflow below runs it once a day on the default branch.
|
|
78
|
+
Run the workflow by hand with `gh workflow run daily-release` to refresh the release pull request sooner.
|
|
79
|
+
|
|
80
|
+
## Running it in CI
|
|
81
|
+
|
|
82
|
+
A repository takes the daily release as `.github/workflows/daily-release.yml`.
|
|
83
|
+
The `pull-request` job runs on the schedule, and the `tag` job runs when a release commit lands on `main`, as [checks-release-tag](checks-release-tag.md) says:
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
name: daily-release
|
|
87
|
+
on:
|
|
88
|
+
schedule:
|
|
89
|
+
- cron: "29 3 * * *"
|
|
90
|
+
workflow_dispatch:
|
|
91
|
+
push:
|
|
92
|
+
branches: [main]
|
|
93
|
+
permissions:
|
|
94
|
+
contents: read
|
|
95
|
+
jobs:
|
|
96
|
+
pull-request:
|
|
97
|
+
if: github.event_name != 'push'
|
|
98
|
+
runs-on: ubuntu-latest
|
|
99
|
+
timeout-minutes: 10
|
|
100
|
+
concurrency:
|
|
101
|
+
group: release-pull-request
|
|
102
|
+
cancel-in-progress: false
|
|
103
|
+
permissions:
|
|
104
|
+
contents: write
|
|
105
|
+
pull-requests: write
|
|
106
|
+
actions: write
|
|
107
|
+
steps:
|
|
108
|
+
- uses: actions/checkout@v5
|
|
109
|
+
with:
|
|
110
|
+
fetch-depth: 0
|
|
111
|
+
- uses: oven-sh/setup-bun@v2
|
|
112
|
+
with:
|
|
113
|
+
bun-version-file: .bun-version
|
|
114
|
+
- run: bun install --frozen-lockfile
|
|
115
|
+
- name: open or refresh the release pull request
|
|
116
|
+
env:
|
|
117
|
+
GH_TOKEN: ${{ github.token }}
|
|
118
|
+
run: ./node_modules/.bin/checks-release-pr ci.yml commitlint.yml
|
|
119
|
+
tag:
|
|
120
|
+
if: "github.event_name == 'push' && startsWith(github.event.head_commit.message, 'chore: release ')"
|
|
121
|
+
runs-on: ubuntu-latest
|
|
122
|
+
timeout-minutes: 10
|
|
123
|
+
permissions:
|
|
124
|
+
contents: write
|
|
125
|
+
actions: write
|
|
126
|
+
steps:
|
|
127
|
+
- uses: actions/checkout@v5
|
|
128
|
+
with:
|
|
129
|
+
fetch-depth: 0
|
|
130
|
+
- uses: oven-sh/setup-bun@v2
|
|
131
|
+
with:
|
|
132
|
+
bun-version-file: .bun-version
|
|
133
|
+
- run: bun install --frozen-lockfile
|
|
134
|
+
- name: tag the release
|
|
135
|
+
env:
|
|
136
|
+
GH_TOKEN: ${{ github.token }}
|
|
137
|
+
run: ./node_modules/.bin/checks-release-tag release.yml
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
A public repository keeps `runs-on: ubuntu-latest`.
|
|
141
|
+
A private repository sets `runs-on: ${{ vars.CI_RUNS_ON || fromJSON('["self-hosted","Linux","X64","winbox"]') }}` on both jobs, as its other workflows do.
|
|
142
|
+
A repository whose build needs more than Bun adds the steps its `.github/workflows/ci.yml` runs before `bun run build` to both jobs, and nothing after it.
|
|
143
|
+
Neither job runs a test suite or a mutation run.
|
|
144
|
+
On a hosted runner the `pull-request` job takes about 25 seconds, which GitHub bills as one minute, whether it opens, refreshes or leaves the pull request alone.
|
|
145
|
+
A private repository runs it on its self-hosted runner, which bills no minutes.
|
|
146
|
+
The `tag` job runs only when a release lands, and a job its `if` skips bills nothing.
|
|
147
|
+
The checks it dispatches are the release pull request's own required checks, and they run again only when `main` moves under it.
|
|
148
|
+
The pull request also lists its own `pull_request` runs as waiting for approval.
|
|
149
|
+
Nothing requires them, and approving them runs the same checks again.
|
|
150
|
+
|
|
151
|
+
The daily release needs the repository's other workflows to accept the dispatch:
|
|
152
|
+
|
|
153
|
+
- `.github/workflows/ci.yml` and `.github/workflows/commitlint.yml` trigger on `workflow_dispatch`, grant `statuses: write`, and end each required job with the step below.
|
|
154
|
+
`commitlint.yml` also grants `pull-requests: read`, which the title lookup needs.
|
|
155
|
+
- The title lint reads the title of the one open pull request its branch heads when the event carries none, as [Commit messages](../configs/commit-messages.md) says.
|
|
156
|
+
- `.github/workflows/release.yml` triggers on `workflow_dispatch` and refuses a ref that is not a tag, as [checks-release-notes](checks-release-notes.md) shows.
|
|
157
|
+
- **Allow GitHub Actions to create and approve pull requests** is on, which the call after the step sets.
|
|
158
|
+
- A release pull request holds current `main` when it merges.
|
|
159
|
+
One that merges behind `main` lands a changelog short of the commits `main` gained, and `checks-release-tag` refuses to tag it.
|
|
160
|
+
|
|
161
|
+
The step reports a dispatched run's result as a commit status on the head commit:
|
|
162
|
+
|
|
163
|
+
```yaml
|
|
164
|
+
- name: report the result on the head commit
|
|
165
|
+
if: always() && github.event_name == 'workflow_dispatch'
|
|
166
|
+
env:
|
|
167
|
+
GH_TOKEN: ${{ github.token }}
|
|
168
|
+
STATE: ${{ job.status == 'success' && 'success' || 'failure' }}
|
|
169
|
+
run: gh api "repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA" -f state="$STATE" -f context="$GITHUB_JOB" -f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" --silent
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The status takes the job's id as its name, so a required job's id is the check name the branch requires.
|
|
173
|
+
Where a status and a check share a name, branch protection requires both, so the status never passes a pull request whose own check failed.
|
|
174
|
+
The call turns the repository setting on:
|
|
175
|
+
|
|
176
|
+
```sh
|
|
177
|
+
gh api --method PUT repos/<owner>/<repo>/actions/permissions/workflow -F can_approve_pull_request_reviews=true
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Related topics
|
|
181
|
+
|
|
182
|
+
- [checks-release-report](checks-release-report.md)
|
|
183
|
+
- [checks-release-tag](checks-release-tag.md)
|
|
184
|
+
- [checks-changelog](checks-changelog.md)
|
|
185
|
+
- [checks-release-notes](checks-release-notes.md)
|
|
@@ -27,7 +27,7 @@ checks-release-report
|
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
It takes no arguments.
|
|
30
|
-
Run it
|
|
30
|
+
Run it to see whether a release is due and what it holds.
|
|
31
31
|
|
|
32
32
|
## Exit codes
|
|
33
33
|
|
|
@@ -55,10 +55,12 @@ release-report: no unreleased changes since v0.1.0
|
|
|
55
55
|
|
|
56
56
|
## When it runs
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
A person runs it to see what the next release holds.
|
|
59
|
+
[checks-release-pr](checks-release-pr.md) reads the same changes before it opens the release pull request.
|
|
59
60
|
A repository with no versioned releases does not need it.
|
|
60
61
|
|
|
61
62
|
## Related topics
|
|
62
63
|
|
|
63
64
|
- [checks-changelog](checks-changelog.md)
|
|
64
65
|
- [checks-release-notes](checks-release-notes.md)
|
|
66
|
+
- [checks-release-pr](checks-release-pr.md)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: reference
|
|
3
|
+
audience: consumers
|
|
4
|
+
---
|
|
5
|
+
# checks-release-tag
|
|
6
|
+
|
|
7
|
+
`checks-release-tag` tags a landed release commit with its version and dispatches the release workflow on the tag.
|
|
8
|
+
|
|
9
|
+
## What it checks
|
|
10
|
+
|
|
11
|
+
It reads the subject of `HEAD`, and does nothing unless the subject is `chore: release <version>`, with or without the ` (#N)` a squash merge adds.
|
|
12
|
+
It refuses a release commit whose `package.json` holds another version.
|
|
13
|
+
It tags `HEAD` as `v<version>` and pushes the tag to `origin`.
|
|
14
|
+
A tag the workflow token pushes starts no `push` workflow, so it then dispatches the workflow its argument names on the tag.
|
|
15
|
+
It does nothing when `v<version>` already tags `HEAD`, so a rerun after a release never publishes it twice.
|
|
16
|
+
When the dispatch fails after the push, it names `gh workflow run <workflow> --ref v<version>`.
|
|
17
|
+
A rerun then finds the tag and does nothing, so that command is what dispatches the release workflow by hand.
|
|
18
|
+
It refuses when `v<version>` already tags another commit.
|
|
19
|
+
Before it pushes the tag, it runs `bun run build` and refuses when the build rewrites a committed file.
|
|
20
|
+
That happens when a release pull request merged behind `main`, so its `CHANGELOG.md` lacks the commits `main` gained.
|
|
21
|
+
The refusal says to open a `chore: cancel the unpublished <version>` pull request that returns `package.json` to the last tag's version and commits what `bun run build` then writes to `CHANGELOG.md`.
|
|
22
|
+
The build reads the returned version as a revert, so it drops the unpublished section, and the next `daily-release` run cuts the release again with every change since the last tag.
|
|
23
|
+
|
|
24
|
+
## What it reads
|
|
25
|
+
|
|
26
|
+
It reads the subject of `HEAD` and `package.json` from the checkout, and the tag from `origin` with `git ls-remote`.
|
|
27
|
+
The build it runs reads the whole history, so the checkout fetches all of it.
|
|
28
|
+
It pushes the tag with the credentials the checkout holds.
|
|
29
|
+
It calls the GitHub API through `gh api`, which takes the repository from the checkout's remote and the token from `GH_TOKEN`.
|
|
30
|
+
The token needs `contents: write` and `actions: write`.
|
|
31
|
+
|
|
32
|
+
## Arguments
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
checks-release-tag <workflow>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The argument names the release workflow file under `.github/workflows/`, such as `release.yml`.
|
|
39
|
+
That workflow triggers on `workflow_dispatch` and keeps its own guards, as [checks-release-notes](checks-release-notes.md) shows.
|
|
40
|
+
|
|
41
|
+
## Exit codes
|
|
42
|
+
|
|
43
|
+
| Code | When |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| 0 | `HEAD` is no release commit, its tag already tags it, or the tag is pushed and the release workflow dispatched |
|
|
46
|
+
| 2 | the arguments do not parse, `package.json` disagrees with the subject, the tag tags another commit, the build fails or rewrites a committed file, or the push or a GitHub API call fails |
|
|
47
|
+
|
|
48
|
+
## Sample output
|
|
49
|
+
|
|
50
|
+
A run that tags the release prints the build's own output, then one line:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
release-tag: tagged 3f2a9c81d0b4 as v0.4.0, and dispatched release.yml on it
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## When it runs
|
|
57
|
+
|
|
58
|
+
The `tag` job of the daily release workflow runs it on each push to `main` whose head commit reads as a release, as [checks-release-pr](checks-release-pr.md#running-it-in-ci) shows.
|
|
59
|
+
|
|
60
|
+
## Related topics
|
|
61
|
+
|
|
62
|
+
- [checks-release-pr](checks-release-pr.md)
|
|
63
|
+
- [checks-release-notes](checks-release-notes.md)
|