@avi2dg/checks 0.30.0 → 0.32.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 +17 -0
- package/README.md +5 -2
- package/dist/readability/index.js +94 -1
- package/docs/configs/commit-messages.md +5 -0
- package/docs/configs/typescript-rules.md +10 -0
- package/docs/design.md +38 -4
- package/docs/gates/checks-commit-identity.md +3 -0
- package/docs/gates/checks-lint-coverage.md +9 -8
- package/docs/gates/checks-lint.md +3 -2
- 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-unused.md +7 -6
- package/oxlintrc.json +7 -0
- package/package.json +22 -3
- package/src/complexity/exports.ts +2 -1
- package/src/complexity/knip.ts +4 -5
- package/src/complexity/unused.ts +4 -3
- package/src/core/gates.ts +7 -4
- package/src/delivery/commit-identity.ts +18 -2
- 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 +195 -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 +79 -0
- package/src/quality/lint-coverage.sh +11 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
|
|
4
4
|
|
|
5
|
+
## 0.32.0
|
|
6
|
+
|
|
7
|
+
Released 2026-09-29.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **delivery:** add checks-secrets gate that fails a range whose commits add a secret [#112](https://github.com/avi2d/checks/pull/112)
|
|
12
|
+
- **delivery:** open a release pull request daily and tag it when it merges [#111](https://github.com/avi2d/checks/pull/111)
|
|
13
|
+
|
|
14
|
+
## 0.31.0
|
|
15
|
+
|
|
16
|
+
Released 2026-09-28.
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
- lint .astro files in lint coverage, unused and a thin-frontmatter rule [#108](https://github.com/avi2d/checks/pull/108)
|
|
21
|
+
|
|
5
22
|
## 0.30.0
|
|
6
23
|
|
|
7
24
|
Released 2026-09-28.
|
package/README.md
CHANGED
|
@@ -118,15 +118,16 @@ The table groups the gates by vector, the part of a repository each one judges.
|
|
|
118
118
|
| --- | --- | --- | --- |
|
|
119
119
|
| complexity | [`checks-suppressions-ratchet`](docs/gates/checks-suppressions-ratchet.md) | the range | every repository |
|
|
120
120
|
| complexity | [`checks-repetition`](docs/gates/checks-repetition.md) | the range | a repository tracking `*.ts` or `*.tsx` |
|
|
121
|
-
| complexity | [`checks-unused`](docs/gates/checks-unused.md) | the working tree | a repository tracking `*.ts` or `*.tsx` |
|
|
121
|
+
| complexity | [`checks-unused`](docs/gates/checks-unused.md) | the working tree | a repository tracking `*.ts` or `*.tsx` or `*.astro` |
|
|
122
122
|
| complexity | [`checks-exports`](docs/gates/checks-exports.md) | the range | a repository tracking `*.ts` or `*.tsx` |
|
|
123
|
-
| quality | [`checks-lint-coverage`](docs/gates/checks-lint-coverage.md) | the working tree | a repository tracking `*.ts` or `*.tsx` |
|
|
123
|
+
| quality | [`checks-lint-coverage`](docs/gates/checks-lint-coverage.md) | the working tree | a repository tracking `*.ts` or `*.tsx` or `*.astro` |
|
|
124
124
|
| quality | [`checks-comment-gate`](docs/gates/checks-comment-gate.md) | the range | every repository |
|
|
125
125
|
| testing | [`checks-test-layout`](docs/gates/checks-test-layout.md) | the working tree | a repository tracking `*.ts` or `*.tsx` |
|
|
126
126
|
| testing | [`checks-quarantine-clock`](docs/gates/checks-quarantine-clock.md) | the range | every repository |
|
|
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.
|
|
@@ -416,11 +416,104 @@ var rule = {
|
|
|
416
416
|
};
|
|
417
417
|
var cognitive_complexity_default = rule;
|
|
418
418
|
|
|
419
|
+
// src/complexity/readability/thin-astro.ts
|
|
420
|
+
function unwrapped(expression) {
|
|
421
|
+
let current = expression;
|
|
422
|
+
while (current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSNonNullExpression" || current.type === "TSTypeAssertion") {
|
|
423
|
+
current = current.expression;
|
|
424
|
+
}
|
|
425
|
+
return current;
|
|
426
|
+
}
|
|
427
|
+
function isAstroProps(value) {
|
|
428
|
+
if (value.type !== "MemberExpression" || value.computed)
|
|
429
|
+
return false;
|
|
430
|
+
const { object, property } = value;
|
|
431
|
+
return object.type === "Identifier" && object.name === "Astro" && property.type === "Identifier" && property.name === "props";
|
|
432
|
+
}
|
|
433
|
+
function isPlainValue(value, bound) {
|
|
434
|
+
return value.type === "Literal" || value.type === "Identifier" && bound.has(value.name);
|
|
435
|
+
}
|
|
436
|
+
function readsProps(expression, bound) {
|
|
437
|
+
const value = unwrapped(expression);
|
|
438
|
+
if (value.type !== "MemberExpression")
|
|
439
|
+
return value.type === "Identifier" && bound.has(value.name);
|
|
440
|
+
if (value.computed && !isPlainValue(value.property, bound))
|
|
441
|
+
return false;
|
|
442
|
+
return isAstroProps(value) || readsProps(value.object, bound);
|
|
443
|
+
}
|
|
444
|
+
function isPlainPattern(pattern, bound) {
|
|
445
|
+
if (pattern === null)
|
|
446
|
+
return true;
|
|
447
|
+
if (pattern.type === "RestElement")
|
|
448
|
+
return isPlainPattern(pattern.argument, bound);
|
|
449
|
+
if (pattern.type === "AssignmentPattern")
|
|
450
|
+
return isPlainValue(pattern.right, bound) && isPlainPattern(pattern.left, bound);
|
|
451
|
+
if (pattern.type === "ArrayPattern")
|
|
452
|
+
return pattern.elements.every((element) => isPlainPattern(element, bound));
|
|
453
|
+
if (pattern.type === "Identifier")
|
|
454
|
+
return true;
|
|
455
|
+
return pattern.properties.every((property) => property.type === "RestElement" ? isPlainPattern(property.argument, bound) : (!property.computed || isPlainValue(property.key, bound)) && isPlainPattern(property.value, bound));
|
|
456
|
+
}
|
|
457
|
+
function isPropsRead(declarator, bound) {
|
|
458
|
+
return declarator.init !== null && readsProps(declarator.init, bound) && isPlainPattern(declarator.id, bound);
|
|
459
|
+
}
|
|
460
|
+
function boundName(pattern, found) {
|
|
461
|
+
if (pattern === null)
|
|
462
|
+
return;
|
|
463
|
+
if (pattern.type === "RestElement")
|
|
464
|
+
boundName(pattern.argument, found);
|
|
465
|
+
else if (pattern.type === "AssignmentPattern")
|
|
466
|
+
boundName(pattern.left, found);
|
|
467
|
+
else if (pattern.type === "ObjectPattern") {
|
|
468
|
+
for (const property of pattern.properties)
|
|
469
|
+
boundName(property.type === "Property" ? property.value : property.argument, found);
|
|
470
|
+
} else if (pattern.type === "ArrayPattern") {
|
|
471
|
+
for (const element of pattern.elements)
|
|
472
|
+
boundName(element, found);
|
|
473
|
+
} else
|
|
474
|
+
found.add(pattern.name);
|
|
475
|
+
}
|
|
476
|
+
function isTypeDeclaration(statement) {
|
|
477
|
+
const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
478
|
+
return declaration?.type === "TSInterfaceDeclaration" || declaration?.type === "TSTypeAliasDeclaration";
|
|
479
|
+
}
|
|
480
|
+
var rule2 = {
|
|
481
|
+
meta: {
|
|
482
|
+
type: "problem",
|
|
483
|
+
docs: { description: 'Disallow logic in .astro frontmatter and script blocks: only imports, props and markup, with client code loaded by a side-effect import such as import "../client.ts"' }
|
|
484
|
+
},
|
|
485
|
+
create(context) {
|
|
486
|
+
if (!context.filename.endsWith(".astro"))
|
|
487
|
+
return {};
|
|
488
|
+
const bound = new Set;
|
|
489
|
+
return {
|
|
490
|
+
Program(node) {
|
|
491
|
+
for (const statement of node.body) {
|
|
492
|
+
if (statement.type === "ImportDeclaration" || statement.type === "EmptyStatement" || statement.type === "ExportNamedDeclaration" && statement.source !== null || isTypeDeclaration(statement)) {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (statement.type === "VariableDeclaration" && statement.declarations.length > 0 && statement.declarations.every((declarator) => isPropsRead(declarator, bound))) {
|
|
496
|
+
for (const declarator of statement.declarations)
|
|
497
|
+
boundName(declarator.id, bound);
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
context.report({
|
|
501
|
+
node: statement,
|
|
502
|
+
message: 'an .astro frontmatter or script block holds more than imports and props: move this statement into a .ts file and import it, so the .astro file holds only imports, props and markup; a script block loads client code with a side-effect import such as import "../client.ts"'
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
var thin_astro_default = rule2;
|
|
510
|
+
|
|
419
511
|
// src/complexity/readability/index.ts
|
|
420
512
|
var plugin = {
|
|
421
513
|
meta: { name: "readability" },
|
|
422
514
|
rules: {
|
|
423
|
-
"cognitive-complexity": cognitive_complexity_default
|
|
515
|
+
"cognitive-complexity": cognitive_complexity_default,
|
|
516
|
+
"thin-astro": thin_astro_default
|
|
424
517
|
}
|
|
425
518
|
};
|
|
426
519
|
var readability_default = plugin;
|
|
@@ -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.
|
|
@@ -43,6 +43,16 @@ The base loads the kit's `data-shape` plugin from `dist/` with one rule for ever
|
|
|
43
43
|
- Derive such a type from the schema with `typeof Name.Type` instead of writing both.
|
|
44
44
|
- The twin rule runs on production files only, so a test that declares its own schema as an oracle stays green.
|
|
45
45
|
|
|
46
|
+
## Astro rules
|
|
47
|
+
|
|
48
|
+
An override in `oxlintrc.json` turns on one rule of the kit's `readability` plugin in each `.astro` file:
|
|
49
|
+
|
|
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.
|
|
52
|
+
- Move a refused statement into a `.ts` file and import it, so the `.astro` file holds only imports, props and markup.
|
|
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
|
+
- A dynamic route re-exports `getStaticPaths` from a `.ts` file, as in `export { getStaticPaths } from "../lib/paths.ts";`.
|
|
55
|
+
|
|
46
56
|
## Rules outside tests
|
|
47
57
|
|
|
48
58
|
An override in `oxlintrc.json` turns on these type-aware rules in each `.ts` and `.tsx` file outside `tests/`:
|
package/docs/design.md
CHANGED
|
@@ -86,7 +86,7 @@ npm adds `package.json`, `README.md` and `LICENSE` whatever `files` says.
|
|
|
86
86
|
`bun pm pack` builds the same tarball the registry serves, and the consumer e2e test installs that tarball.
|
|
87
87
|
|
|
88
88
|
Each oxlint plugin ships compiled under `dist/`, because Node refuses to strip types from a `.ts` file under `node_modules`.
|
|
89
|
-
`@oxlint/plugins` ships no RuleTester, so each `effect-channel`, `readability` and `data-shape` rule is proven red and green against an installed consumer in `tests/e2e/consumer.test.ts
|
|
89
|
+
`@oxlint/plugins` ships no RuleTester, so each `effect-channel`, `readability` and `data-shape` rule is proven red and green against an installed consumer in `tests/e2e/consumer.test.ts`, except `readability/thin-astro`, which `tests/e2e/astro-consumer.test.ts` proves against a consumer tree linked to the checkout.
|
|
90
90
|
`dist/` is committed, with the doc templates in `dist/templates/`, and so is `CHANGELOG.md`, which the same build writes.
|
|
91
91
|
No `prepack` or `prepublishOnly` script rebuilds them, so a publish ships the committed files.
|
|
92
92
|
CI runs `git diff --exit-code` over the whole tree after `bun run build`.
|
|
@@ -109,7 +109,7 @@ So the three packages move together at one exact version.
|
|
|
109
109
|
A gate then behaves the same alone or through `checks-lint`, and `lint-coverage.sh` can stay a shell script.
|
|
110
110
|
The gates run one at a time and pass their output straight through, so each report reads whole and in the order of the gate table.
|
|
111
111
|
`checks-lint` picks the gates that apply from the tracked files, and a repository cannot select gates.
|
|
112
|
-
A TypeScript gate runs as soon as the repository tracks TypeScript source.
|
|
112
|
+
A TypeScript gate runs as soon as the repository tracks TypeScript source, and `checks-lint-coverage` and `checks-unused` also run on tracked Astro source.
|
|
113
113
|
|
|
114
114
|
GitHub authors the pull request merge commit it builds as `GitHub <noreply@github.com>`, and `checks-commit-identity` refuses that author.
|
|
115
115
|
So `checks-lint` ends a pull request's range at the event's head commit, and the merge commit is never in it.
|
|
@@ -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
|
|
|
@@ -238,6 +254,24 @@ The kit caps each entry at 30 days and measures a range from the head's dates, a
|
|
|
238
254
|
`--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
255
|
The file is JSON because `Bun.TOML` cannot parse a TOML date.
|
|
240
256
|
|
|
257
|
+
## Secrets fail in every commit of the range
|
|
258
|
+
|
|
259
|
+
`checks-secrets` scans each commit in the range rather than the files at the head.
|
|
260
|
+
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.
|
|
261
|
+
So the fix is to rewrite the commit that added it, and to rotate the secret once it has left the machine.
|
|
262
|
+
|
|
263
|
+
In a merge commit the gate scans only the merge's own resolution, the difference from the merge git would make on its own.
|
|
264
|
+
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.
|
|
265
|
+
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.
|
|
266
|
+
|
|
267
|
+
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.
|
|
268
|
+
The kit's own rules in `src/delivery/gitleaks.toml` add only the VPN keys and proxy links those rules miss.
|
|
269
|
+
The kit pins gitleaks by version and SHA-256 for the reason it pins OSV-Scanner.
|
|
270
|
+
|
|
271
|
+
No finding can be accepted.
|
|
272
|
+
A secret has no false positive worth keeping in git, because a placeholder carries the same shape without the value.
|
|
273
|
+
So the gate ignores a repository's `.gitleaks.toml`, `.gitleaksignore` and `gitleaks:allow` comments, which would each let one repository pass what another fails.
|
|
274
|
+
|
|
241
275
|
## Related topics
|
|
242
276
|
|
|
243
277
|
- [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
|
|
|
@@ -4,12 +4,13 @@ audience: consumers
|
|
|
4
4
|
---
|
|
5
5
|
# checks-lint-coverage
|
|
6
6
|
|
|
7
|
-
`checks-lint-coverage` is the gate that fails when oxlint skips a tracked TypeScript file, or when the program `tsconfig.json` builds drops the ts-reset rules, without saying so.
|
|
7
|
+
`checks-lint-coverage` is the gate that fails when oxlint skips a tracked TypeScript or Astro file, or when the program `tsconfig.json` builds drops the ts-reset rules, without saying so.
|
|
8
8
|
|
|
9
9
|
## What it checks
|
|
10
10
|
|
|
11
|
-
It fails when oxlint skips a tracked `.ts` or `.
|
|
11
|
+
It fails when oxlint skips a tracked `.ts`, `.tsx` or `.astro` file, for example through a stray `.gitignore` entry.
|
|
12
12
|
It compares `git ls-files` against oxlint's own file walk and names the missing files.
|
|
13
|
+
It lists each tracked `.astro` file the same way, since oxlint lints the frontmatter and script blocks of an `.astro` file, where a script block loads client code with a side-effect import such as `import "../client.ts";`.
|
|
13
14
|
|
|
14
15
|
It fails when the program `tsconfig.json` builds leaves out the `is-array` or the `json-parse` rule of `@total-typescript/ts-reset`, which `tsconfig.effect.json` lists.
|
|
15
16
|
A `tsconfig.json` that does not extend `@avi2dg/checks/tsconfig.effect.json`, or that sets both `files` and `include`, leaves both rules out.
|
|
@@ -17,9 +18,9 @@ A `tsconfig.json` that does not extend `@avi2dg/checks/tsconfig.effect.json`, or
|
|
|
17
18
|
|
|
18
19
|
## What it reads
|
|
19
20
|
|
|
20
|
-
It reads the working tree: the `*.ts` and `*.
|
|
21
|
+
It reads the working tree: the `*.ts`, `*.tsx` and `*.astro` files `git ls-files` lists, and the files `oxlint --debug=files` walks.
|
|
21
22
|
It walks without naming a path, since an explicit path bypasses the ignore files whose skips it looks for.
|
|
22
|
-
It reads the program from `tsc --listFilesOnly -p tsconfig.json`, and passes over the program when the root holds no `tsconfig.json`.
|
|
23
|
+
It reads the program from `tsc --listFilesOnly -p tsconfig.json`, and passes over the program when the repository tracks no `.ts` or `.tsx` file or the root holds no `tsconfig.json`.
|
|
23
24
|
oxlint and tsc must be on `PATH`, as they are under a package script.
|
|
24
25
|
|
|
25
26
|
## Arguments
|
|
@@ -30,14 +31,14 @@ It takes none.
|
|
|
30
31
|
|
|
31
32
|
| Code | When |
|
|
32
33
|
| --- | --- |
|
|
33
|
-
| 0 | the repository tracks no `.ts` or `.
|
|
34
|
+
| 0 | the repository tracks no `.ts`, `.tsx` or `.astro` file, or oxlint walks each one and the program holds both ts-reset rules, the repository tracks no `.ts` or `.tsx` file or the root holds no `tsconfig.json` |
|
|
34
35
|
| 1 | oxlint skips a tracked file, or the program drops a ts-reset rule |
|
|
35
36
|
| 2 | oxlint cannot walk the tree, or tsc cannot list the program after oxlint walks every tracked file, as when either is not on `PATH` or a config does not parse |
|
|
36
37
|
|
|
37
38
|
## Sample output
|
|
38
39
|
|
|
39
40
|
```
|
|
40
|
-
lint-coverage: oxlint skips 1/3 tracked .ts/.tsx files; missing:
|
|
41
|
+
lint-coverage: oxlint skips 1/3 tracked .ts/.tsx/.astro files; missing:
|
|
41
42
|
ignored/b.ts
|
|
42
43
|
lint-coverage: the program tsconfig.json builds drops the ts-reset rules: is-array json-parse
|
|
43
44
|
extend @avi2dg/checks/tsconfig.effect.json, and set files or include in tsconfig.json but not both
|
|
@@ -46,13 +47,13 @@ lint-coverage: the program tsconfig.json builds drops the ts-reset rules: is-arr
|
|
|
46
47
|
A passing run counts the files and names the rules:
|
|
47
48
|
|
|
48
49
|
```
|
|
49
|
-
lint-coverage: 71/71 tracked .ts/.tsx files
|
|
50
|
+
lint-coverage: 71/71 tracked .ts/.tsx/.astro files
|
|
50
51
|
lint-coverage: the program tsconfig.json builds holds the ts-reset rules is-array and json-parse
|
|
51
52
|
```
|
|
52
53
|
|
|
53
54
|
## When it runs
|
|
54
55
|
|
|
55
|
-
`checks-lint` runs it when the repository tracks a `.ts` or `.
|
|
56
|
+
`checks-lint` runs it when the repository tracks a `.ts`, `.tsx` or `.astro` file.
|
|
56
57
|
|
|
57
58
|
## Related topics
|
|
58
59
|
|
|
@@ -10,6 +10,7 @@ audience: consumers
|
|
|
10
10
|
|
|
11
11
|
It runs the gates under [What runs](../../README.md#what-runs), each in its own process.
|
|
12
12
|
Gates requiring tracked TypeScript files begin running when the repository tracks TypeScript.
|
|
13
|
+
`checks-lint-coverage` and `checks-unused` also begin running when the repository tracks an `.astro` file.
|
|
13
14
|
`checks-advisories` begins running when the repository tracks `bun.lock`.
|
|
14
15
|
All other gates run for every repository.
|
|
15
16
|
|
|
@@ -45,7 +46,7 @@ With two arguments, the base and head override range discovery.
|
|
|
45
46
|
|
|
46
47
|
```
|
|
47
48
|
checks-lint: range 2504acf098d120e73a8ece3c96f22b934f35c6a8..10ba7d8935b73ed72624120a1542e51bd21ca7c7 from HEAD against origin/main
|
|
48
|
-
checks-lint: 1 of
|
|
49
|
+
checks-lint: 1 of 13 gate(s) failed: checks-comment-gate
|
|
49
50
|
```
|
|
50
51
|
|
|
51
52
|
<!-- end generated lint-sample -->
|
|
@@ -53,7 +54,7 @@ checks-lint: 1 of 12 gate(s) failed: checks-comment-gate
|
|
|
53
54
|
## When it runs
|
|
54
55
|
|
|
55
56
|
A repository runs it from `bun run lint` in a pull request workflow that fetches the whole git history.
|
|
56
|
-
It leaves out the TypeScript gates while the repository tracks no TypeScript file.
|
|
57
|
+
It leaves out the TypeScript gates while the repository tracks no TypeScript file, except that `checks-lint-coverage` and `checks-unused` run when it tracks an `.astro` file.
|
|
57
58
|
|
|
58
59
|
## Related topics
|
|
59
60
|
|
|
@@ -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)
|