@avi2dg/checks 0.31.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 +9 -0
- package/README.md +3 -0
- package/docs/configs/commit-messages.md +5 -0
- package/docs/design.md +36 -2
- package/docs/gates/checks-commit-identity.md +3 -0
- package/docs/gates/checks-lint.md +1 -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/package.json +13 -2
- package/src/core/gates.ts +1 -0
- 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 +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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
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
|
+
|
|
5
14
|
## 0.31.0
|
|
6
15
|
|
|
7
16
|
Released 2026-09-28.
|
package/README.md
CHANGED
|
@@ -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.
|
|
@@ -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.
|
package/docs/design.md
CHANGED
|
@@ -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
|
|
|
@@ -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 -->
|
|
@@ -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)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: reference
|
|
3
|
+
audience: consumers
|
|
4
|
+
---
|
|
5
|
+
# checks-secrets
|
|
6
|
+
|
|
7
|
+
`checks-secrets` is the gate that fails a range whose commits add a secret, such as an API token, a private key, a VPN key or a proxy link that carries its credential.
|
|
8
|
+
|
|
9
|
+
## What it checks
|
|
10
|
+
|
|
11
|
+
It scans the lines each commit in the range adds, in every file the commit adds or modifies, with gitleaks.
|
|
12
|
+
In a merge commit it scans the lines the merge's own resolution adds, such as a key written while resolving a conflict, and not what the merge brings in from the other parent.
|
|
13
|
+
It exits 2 when a commit it scans is an octopus merge, one with three or more parents, because git gives no resolution diff for one.
|
|
14
|
+
It fails on each secret the scan finds, and names its file, its line, the rule that matched and the commit that added it.
|
|
15
|
+
A secret one commit adds and a later commit in the range removes still fails, because the first commit still holds it.
|
|
16
|
+
It guards new changes only, and never scans a commit before the range, so a secret already in the history takes no part.
|
|
17
|
+
|
|
18
|
+
It runs gitleaks' default rules, which know the API keys and tokens of common services and private keys in PEM form, and eight rules of the kit's own that report a secret:
|
|
19
|
+
|
|
20
|
+
| Rule | What it matches | What passes |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| `wireguard-key` | a WireGuard or AmneziaWG private or pre-shared key value, 44 base64 characters, under a name that ends in `PrivateKey`, `PresharedKey` or `psk`, such as a real key after `PrivateKey =`, `"privateKey":`, `wg_private_key:` or `client_psk:` | a placeholder such as `<private-key>` or `${WG_PRIVATE_KEY}` |
|
|
23
|
+
| `wireguard-key-file` | a line that holds only a WireGuard or AmneziaWG key, 44 base64 characters, in a file whose name holds `priv`, `preshared`, `pre_shared`, `pre-shared` or `psk`, such as the `privatekey` file `wg genkey \| tee privatekey` writes, whatever else the name holds | a bare key in a file with any other name, such as `publickey` or `peer.pub`, and a placeholder such as `<private-key>` |
|
|
24
|
+
| `wireguard-dot-key-file` | a line that holds only a WireGuard or AmneziaWG key in a file whose name ends in `.key`, such as `wg0.key` | a file whose name ends in `pub.key` or `public.key`, such as `server-public.key`, unless `wireguard-key-file` matches its name |
|
|
25
|
+
| `proxy-userinfo-link` | a `vless://`, `vmess://`, `ss://`, `trojan://`, `hysteria://`, `hysteria2://`, `hy2://`, `tuic://`, `socks://`, `socks5://`, `socks5h://`, `wireguard://` or `wg://` link whose credential before an `@` has a Shannon entropy above 2.5 bits per character, such as a real password before the `@` of a `trojan://` link or a real private key before the `@` of a `wireguard://` link, and a template reference with real text around it, such as a literal user before `:${PASS}` or a non-empty default in `${PASS:-…}` | a link with no credential such as `socks5://127.0.0.1:1080`, a low-entropy credential such as `pw@` or `user@`, a placeholder credential such as `vless://<uuid>@<host>`, a credential that is only template references, such as `{password}`, `$PASS`, `{{ password }}` or `%(password)s` naming a variable that starts with a letter or underscore, or any `${…}` interpolation except a shell default such as `${PASS:-value}`, such as `${pass}`, `${pass:-}`, `${pass:?message}` or `${encodeURIComponent(pass)}`, including a user part such as `$USER:$PASS@`, and a link to `example.com`, `example.net`, `example.org`, a host under `.example`, `.invalid` or `.test`, or `localhost` |
|
|
26
|
+
| `proxy-base64-link` | a `vmess://`, `ss://` or `ssr://` link whose payload is 16 or more base64 characters with a Shannon entropy above 4.2 bits per character, whatever follows it | a placeholder such as `vmess://<base64-config>`, a link to a host and port such as `ss://vpn.home.net:8388`, and a payload an `@` follows, which `proxy-userinfo-link` judges |
|
|
27
|
+
| `proxy-query-credential-first` | the first `auth`, `auth_str`, `obfsParam`, `obfs-password`, `password` or `pk` query value of one of the links `proxy-userinfo-link` names or an `ssr://` link, whose Shannon entropy is above 2.5 bits per character, such as a real password in the `auth` of a `hysteria://` link or a real private key in the `pk` of a `wg://` link, whatever parameters come before or after it | a low-entropy value such as `auth=pw`, a placeholder value such as `auth=<password>`, a value that is only template references as `proxy-userinfo-link` describes, such as `auth={auth}`, `auth=${HY_AUTH:?set HY_AUTH}` or `auth=${encodeURIComponent("a b")}`, and a link to one of the example hosts above |
|
|
28
|
+
| `proxy-query-credential-last` | the last of these query values in such a link, judged the same way, so a real value after a low-entropy or template one still fails | what passes `proxy-query-credential-first` |
|
|
29
|
+
| `proxy-subscription-url` | 16 or more token characters with a Shannon entropy above 3.0 bits per character after the `sub`, `subs`, `subscribe`, `subscription` or `link` path segment of an `http` or `https` URL, or in any `token`, `key`, `uuid` or `id` query value on a line that holds such a URL, each judged on its own | a `token`, `key`, `uuid` or `id` value on a line with no such URL, and a URL to one of the example hosts above |
|
|
30
|
+
|
|
31
|
+
A ninth rule, `proxy-subscription-path`, reports nothing: it finds the URL `proxy-subscription-url` needs on the same line.
|
|
32
|
+
|
|
33
|
+
These rules do not catch:
|
|
34
|
+
|
|
35
|
+
- a real query value between two others of these parameters in one link whose values are low-entropy or template references, such as a real `auth` between an `obfsParam=pw` or `obfsParam=${OBFS}` and a `password=pw`
|
|
36
|
+
- a real `auth`, `auth_str`, `obfsParam`, `obfs-password`, `password` or `pk` query value of an `http` or `https` URL, or of a scheme the table does not name, because the kit's query rules judge share links only, and gitleaks' default rules catch such a value only now and then
|
|
37
|
+
- a credential that begins with `${` and is not a shell default such as `${PASS:-value}`, `${PASS-value}`, `${PASS:=value}` or `${PASS:+value}`, because it reads as an interpolation, even when it holds a fallback such as `${auth ?? "value"}`
|
|
38
|
+
|
|
39
|
+
No setting accepts a finding.
|
|
40
|
+
It ignores a repository's `.gitleaks.toml`, its `.gitleaksignore` and every `gitleaks:allow` comment.
|
|
41
|
+
It skips the paths gitleaks' default config skips, such as lockfiles, images and `node_modules/`.
|
|
42
|
+
|
|
43
|
+
## What it reads
|
|
44
|
+
|
|
45
|
+
It reads the commits of the range from git, not the working tree.
|
|
46
|
+
It needs git 2.36 or newer, which diffs a merge against the merge git would make on its own, and it exits 2 on an older git.
|
|
47
|
+
It reads its rules from `src/delivery/gitleaks.toml` in the installed package.
|
|
48
|
+
|
|
49
|
+
It runs gitleaks 8.30.1, pinned by the SHA-256 of each platform's archive and of the binary inside it.
|
|
50
|
+
On first use it downloads the archive from the scanner's GitHub release and unpacks the binary into `~/.cache/avi2dg-checks/gitleaks/8.30.1/`.
|
|
51
|
+
It checks the binary's SHA-256 again on every run and exits 2 on a copy that differs.
|
|
52
|
+
Builds are pinned for macOS and Linux, each on x64 and arm64.
|
|
53
|
+
On any other platform it exits 2, and no setting runs a scanner other than the pinned build.
|
|
54
|
+
A cold cache downloads about 8 MB.
|
|
55
|
+
|
|
56
|
+
## Arguments
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
checks-secrets <base-ref> <head-ref>
|
|
60
|
+
checks-secrets <ref>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
With two arguments it scans each commit from the merge base of the two refs to the head.
|
|
64
|
+
With one it scans that commit alone.
|
|
65
|
+
|
|
66
|
+
## Exit codes
|
|
67
|
+
|
|
68
|
+
| Code | When |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| 0 | no commit in the range adds a secret |
|
|
71
|
+
| 1 | a commit in the range adds a secret |
|
|
72
|
+
| 2 | a ref does not resolve, or a commit it scans is an octopus merge, or no verified scanner is at hand, or git is older than 2.36, or gitleaks fails |
|
|
73
|
+
|
|
74
|
+
## Sample output
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
secrets: the range adds 3 secret(s); put a placeholder such as <private-key> in its place in the commit that added it, and rotate any secret that left this machine:
|
|
78
|
+
vpn/links.txt:1 proxy-userinfo-link in e19e45c2: A proxy share link that carries its credential before the @, such as vless://, trojan://, socks5:// or wireguard://
|
|
79
|
+
vpn/wg0.conf:2 wireguard-key in e19e45c2: A WireGuard or AmneziaWG private or pre-shared key
|
|
80
|
+
vpn/wg0.conf:7 wireguard-key in e19e45c2: A WireGuard or AmneziaWG private or pre-shared key
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The report never prints a secret.
|
|
84
|
+
|
|
85
|
+
## When it runs
|
|
86
|
+
|
|
87
|
+
`checks-lint` runs it over each pull request's range in every repository, as [checks-lint](checks-lint.md) says.
|
|
88
|
+
A local `bun run lint` runs it over the commits the branch adds, so it fails before a push.
|
|
89
|
+
|
|
90
|
+
## Related topics
|
|
91
|
+
|
|
92
|
+
- [checks-lint](checks-lint.md)
|
|
93
|
+
- [Why it is shaped this way](../design.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avi2dg/checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Deterministic checks shared across a set of TypeScript repositories",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
"src/delivery/changelog-write.ts",
|
|
30
30
|
"src/delivery/release-notes.ts",
|
|
31
31
|
"src/delivery/release-report.ts",
|
|
32
|
+
"src/delivery/release.ts",
|
|
33
|
+
"src/delivery/release-pr.ts",
|
|
34
|
+
"src/delivery/release-tag.ts",
|
|
35
|
+
"src/delivery/github.ts",
|
|
32
36
|
"src/testing/test-layout.ts",
|
|
33
37
|
"src/testing/test.ts",
|
|
34
38
|
"src/testing/test-skips.ts",
|
|
@@ -43,6 +47,9 @@
|
|
|
43
47
|
"src/testing/subsumed-tests.ts",
|
|
44
48
|
"src/delivery/ci-wiring.ts",
|
|
45
49
|
"src/delivery/shell-command.ts",
|
|
50
|
+
"src/delivery/secrets.ts",
|
|
51
|
+
"src/delivery/gitleaks.ts",
|
|
52
|
+
"src/delivery/gitleaks.toml",
|
|
46
53
|
"src/quality/comment-matchers.ts",
|
|
47
54
|
"src/docs/prose-matchers.ts",
|
|
48
55
|
"src/quality/comments.ts",
|
|
@@ -60,6 +67,7 @@
|
|
|
60
67
|
"src/dependencies/advisories.ts",
|
|
61
68
|
"src/dependencies/advisory-rules.ts",
|
|
62
69
|
"src/dependencies/osv-scanner.ts",
|
|
70
|
+
"src/dependencies/pinned-binary.ts",
|
|
63
71
|
"src/docs/doc-agents.ts",
|
|
64
72
|
"src/docs/doc-outline.ts",
|
|
65
73
|
"src/docs/doc-rules.ts",
|
|
@@ -93,6 +101,8 @@
|
|
|
93
101
|
"checks-changelog": "src/delivery/changelog-write.ts",
|
|
94
102
|
"checks-release-notes": "src/delivery/release-notes.ts",
|
|
95
103
|
"checks-release-report": "src/delivery/release-report.ts",
|
|
104
|
+
"checks-release-pr": "src/delivery/release-pr.ts",
|
|
105
|
+
"checks-release-tag": "src/delivery/release-tag.ts",
|
|
96
106
|
"checks-lint-coverage": "src/quality/lint-coverage.sh",
|
|
97
107
|
"checks-test-layout": "src/testing/test-layout.ts",
|
|
98
108
|
"checks-test": "src/testing/test.ts",
|
|
@@ -110,7 +120,8 @@
|
|
|
110
120
|
"checks-quarantine-clock": "src/testing/quarantine-clock.ts",
|
|
111
121
|
"checks-docs": "src/docs/docs.ts",
|
|
112
122
|
"checks-vendor": "src/dependencies/vendor.ts",
|
|
113
|
-
"checks-advisories": "src/dependencies/advisories.ts"
|
|
123
|
+
"checks-advisories": "src/dependencies/advisories.ts",
|
|
124
|
+
"checks-secrets": "src/delivery/secrets.ts"
|
|
114
125
|
},
|
|
115
126
|
"scripts": {
|
|
116
127
|
"prepare": "bun src/dependencies/vendor.ts --library effect --package effect --repository https://github.com/Effect-TS/effect.git --tag 'effect@{version}' --path packages/effect/package.json",
|
package/src/core/gates.ts
CHANGED
|
@@ -43,6 +43,7 @@ export const KIT_GATES = [
|
|
|
43
43
|
{ bin: "checks-comment-gate", vector: "quality", file: "comment-gate.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
44
44
|
{ bin: "checks-suppressions-ratchet", vector: "complexity", file: "suppressions-ratchet.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
45
45
|
{ bin: "checks-ci-wiring", vector: "delivery", file: "ci-wiring.ts", reads: "tree", appliesTo: EVERY_REPOSITORY },
|
|
46
|
+
{ bin: "checks-secrets", vector: "delivery", file: "secrets.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
46
47
|
{ bin: "checks-docs", vector: "docs", file: "docs.ts", reads: "range", alsoReads: "every agent file at the head commit", appliesTo: EVERY_REPOSITORY },
|
|
47
48
|
{ bin: "checks-repetition", vector: "complexity", file: "repetition.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
|
|
48
49
|
{ bin: "checks-unused", vector: "complexity", file: "unused.ts", reads: "tree", appliesTo: LINTED_SOURCE },
|