@avi2dg/checks 0.42.0 → 0.44.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 +20 -0
- package/README.md +5 -2
- package/{commitlint.config.js → commitlint.config.ts} +1 -1
- package/dist/mutation/mutation-guard-plugin.js +40 -0
- package/dist/mutation/mutation-refusal.js +23 -0
- package/docs/configs/commit-messages.md +2 -2
- package/docs/design.md +20 -0
- package/docs/gates/checks-ci-wiring.md +3 -2
- package/docs/gates/checks-copy.md +113 -0
- package/docs/gates/checks-docs-only.md +3 -2
- package/docs/gates/checks-docs.md +3 -0
- package/docs/gates/checks-flake.md +49 -1
- package/docs/gates/checks-lint.md +2 -1
- package/docs/gates/checks-mutation-baseline.md +4 -2
- package/docs/gates/checks-mutation-compare.md +33 -8
- package/docs/gates/checks-mutation-scope.md +140 -0
- package/docs/gates/checks-mutation.md +11 -7
- package/docs/gates/checks-no-plain-js.md +60 -0
- package/docs/gates/checks-test-layout.md +3 -2
- package/docs/gates/checks-test.md +35 -6
- package/package.json +18 -6
- package/src/core/declared-inputs.ts +76 -0
- package/src/core/gates.ts +4 -0
- package/src/core/git.ts +9 -0
- package/src/delivery/ci-wiring.ts +25 -11
- package/src/delivery/commit-title.ts +2 -2
- package/src/docs/copy-html.ts +110 -0
- package/src/docs/copy-text.ts +70 -0
- package/src/docs/copy.ts +70 -0
- package/src/docs/prose-matchers.ts +14 -14
- package/src/quality/frontend-syntax.ts +9 -52
- package/src/quality/no-plain-js.ts +71 -0
- package/src/testing/flake-preload.ts +4 -0
- package/src/testing/flake.ts +142 -16
- package/src/testing/{mutation-scope.js → mutation-refusal.ts} +11 -9
- package/src/testing/mutation-scope.ts +354 -0
- package/src/testing/mutation.ts +1 -1
- package/src/testing/test-layout.ts +8 -15
- package/src/testing/test-report.ts +20 -5
- package/src/testing/test-skips.ts +1 -1
- package/src/testing/test.ts +53 -11
- package/stryker.preset.js +3 -3
- package/src/testing/mutation-guard-plugin.js +0 -13
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: reference
|
|
3
|
+
audience: consumers
|
|
4
|
+
---
|
|
5
|
+
# checks-mutation-scope
|
|
6
|
+
|
|
7
|
+
`checks-mutation-scope` turns a pull request's diff into the files its mutation runs mutate, and prints them as lines for `GITHUB_ENV`.
|
|
8
|
+
|
|
9
|
+
## What it checks
|
|
10
|
+
|
|
11
|
+
It picks a scope rather than judging one, so it fails only when it cannot pick one.
|
|
12
|
+
It reads `git diff --name-status -M` from the merge base of `<base>` and `HEAD` to `HEAD`.
|
|
13
|
+
A changed source enters the scope when the repository's own Stryker `mutate` globs match it, with each negated glob applied in order.
|
|
14
|
+
It resolves each glob and each path against the working directory, as Stryker does, so an absolute glob matches too.
|
|
15
|
+
A `mutate` entry with a mutation range, such as `src/a.ts:1-10`, fails the run, since a scope of whole files cannot keep the range.
|
|
16
|
+
`--mutate` replaces those globs, so a path the config excludes, such as `packages/*/src/live/**` or `packages/*/src/bin.ts`, stays out of the scope.
|
|
17
|
+
A renamed source enters under both names, and a deleted source does not enter.
|
|
18
|
+
|
|
19
|
+
A unit test is a `.test.ts` or `.test.tsx` file under `tests/unit/`, the files the preset's `unitTestFiles` lists.
|
|
20
|
+
A unit test the pull request adds, edits, deletes or renames is changed.
|
|
21
|
+
A unit test that reads a changed file under `tests/` is changed too.
|
|
22
|
+
It reads a file when it imports it, directly or through another file under `tests/`.
|
|
23
|
+
An import resolves as Bun resolves it, to the file it names, then with an extension added, and only then to the directory's `index`.
|
|
24
|
+
A unit test also reads a changed fixture under `tests/fixtures/` when it names the fixture's path or a directory above it.
|
|
25
|
+
Bun loads each `[test] preload` in `bunfig.toml` before every test file.
|
|
26
|
+
So every unit test is changed when the preload list differs between the merge base and `HEAD`.
|
|
27
|
+
The same holds when the pull request changes a preload or a file a preload imports, wherever that file lives.
|
|
28
|
+
|
|
29
|
+
Each source whose mutants a changed unit test covers or kills in the full baseline report then enters the scope, when it still exists at `HEAD`.
|
|
30
|
+
So a kill lost through a test change alone still shows in the comparison, in a source the pull request leaves alone.
|
|
31
|
+
A changed unit test with no baseline report fails the run rather than reading incomplete coverage.
|
|
32
|
+
Stryker records each test file's source in the report's `testFiles`.
|
|
33
|
+
A changed unit test the merge base holds must have that recorded source match its content at the merge base.
|
|
34
|
+
Otherwise the report ran an older test, so its coverage cannot say what the test covers now.
|
|
35
|
+
The scope then widens to every file at `HEAD` the `mutate` globs match.
|
|
36
|
+
A unit test the pull request adds is new to every report, so it needs no entry.
|
|
37
|
+
Stryker writes no source without mutants into the report, so a mutated source the report lacks may be newer than it.
|
|
38
|
+
When a unit test changed, each such source that exists at `HEAD` enters the scope.
|
|
39
|
+
A change under `tests/e2e/` changes no unit test, so it needs no baseline report.
|
|
40
|
+
The scope trusts the coverage edges of the latest full baseline.
|
|
41
|
+
So a helper, fixture or preload changed on `main` since that baseline can hide a lost kill until the scheduled full baseline refreshes.
|
|
42
|
+
|
|
43
|
+
## What it reads
|
|
44
|
+
|
|
45
|
+
- The diff from the merge base to `HEAD`, and the files git tracks at both.
|
|
46
|
+
- The Stryker config file `stryker run` loads from the working directory, such as `stryker.conf.mjs`, for its `mutate` globs.
|
|
47
|
+
It imports a JavaScript config, so the step runs after `bun install`.
|
|
48
|
+
With no config file, or a config with no `mutate`, it takes Stryker's default globs.
|
|
49
|
+
- `bunfig.toml` at the merge base and at `HEAD`, for its `[test] preload` entries.
|
|
50
|
+
- Each preload at `HEAD`, and each file it imports through a relative path, for the files the preloads load.
|
|
51
|
+
- Each tracked `.ts` and `.tsx` file under `tests/` from the working tree, for its relative imports and the fixture paths it names.
|
|
52
|
+
- Each changed unit test at the merge base, to match it against the source the report recorded.
|
|
53
|
+
- The full baseline report that `checks-mutation-baseline --full` restores, for each mutant's `coveredBy` and `killedBy` and the `testFiles` that own those tests.
|
|
54
|
+
|
|
55
|
+
Stryker leaves `coveredBy` empty on a static mutant, so only `killedBy` names its tests.
|
|
56
|
+
The report must come from a run that restored no state and passed `--disableBail`, which lists every killing test.
|
|
57
|
+
|
|
58
|
+
## Arguments
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
checks-mutation-scope <base> [<full-baseline-report>]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`<base>` is the branch the pull request merges into, such as `origin/main`, or the merge base itself.
|
|
65
|
+
`<full-baseline-report>` is the path the full baseline was restored to, and a path with no file reads as no report.
|
|
66
|
+
|
|
67
|
+
## Output
|
|
68
|
+
|
|
69
|
+
It prints one line for each list, as `NAME=a,b`, with each list sorted by path:
|
|
70
|
+
|
|
71
|
+
| Line | What it lists |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `SCOPE` | the files the base and head runs both mutate, empty when nothing in mutation scope changed |
|
|
74
|
+
| `STALE_TESTS` | the unit tests that count as changed only because they read a changed file |
|
|
75
|
+
|
|
76
|
+
Stryker reads each `--mutate` entry as a glob and turns each backslash into a `/`.
|
|
77
|
+
So `SCOPE` wraps each glob character in a path in a class of its own.
|
|
78
|
+
`app/routes/[id].ts` prints as `app/routes/[[]id[]].ts`, which matches that file and not `app/routes/i.ts`.
|
|
79
|
+
`STALE_TESTS` names each test by its literal path, as the incremental state keys it.
|
|
80
|
+
|
|
81
|
+
Stryker compares only a test file's own source when it resumes from incremental state.
|
|
82
|
+
A run that resumes from the merge base's state drops each `STALE_TESTS` entry from that state first, so Stryker retests the mutants those tests cover.
|
|
83
|
+
|
|
84
|
+
On stderr it names each unit test that reads a changed file, and each source a changed unit test brings in.
|
|
85
|
+
It also names each stale test that widens the scope.
|
|
86
|
+
|
|
87
|
+
## Exit codes
|
|
88
|
+
|
|
89
|
+
| Code | When |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| 0 | it printed the scope, an empty one included |
|
|
92
|
+
| 2 | the arguments do not parse, git fails, the Stryker config, `bunfig.toml` or the baseline report does not load, a `mutate` entry names a mutation range, or a unit test changed with no baseline report |
|
|
93
|
+
|
|
94
|
+
## Sample output
|
|
95
|
+
|
|
96
|
+
A helper change that reaches a source the pull request leaves alone:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
mutation-scope: tests/unit/dashboard/snapshot.test.ts reads tests/lib/session-fixture.ts
|
|
100
|
+
mutation-scope: packages/ui/src/layout.ts enters through tests/unit/dashboard/snapshot.test.ts
|
|
101
|
+
SCOPE=packages/session-quota/src/notes.ts,packages/ui/src/layout.ts
|
|
102
|
+
STALE_TESTS=tests/unit/dashboard/snapshot.test.ts
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
A changed unit test with no full baseline on `main`, on stderr:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
mutation-scope: tests/unit/dashboard/snapshot.test.ts changed, but no baseline report at /runner/temp/baseline-full/mutation.json says which sources they cover
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
A changed unit test that `main` edited after its newest full baseline, on stderr:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
mutation-scope: the baseline report did not run tests/unit/dashboard/snapshot.test.ts as the merge base holds it, so every mutated source enters
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## When it runs
|
|
118
|
+
|
|
119
|
+
Only a CI step the repository writes runs it, in the pull request's mutation comparison, before either Stryker run:
|
|
120
|
+
|
|
121
|
+
```yaml
|
|
122
|
+
- name: Select mutation scope
|
|
123
|
+
env:
|
|
124
|
+
GH_TOKEN: ${{ github.token }}
|
|
125
|
+
run: |
|
|
126
|
+
base="$(git merge-base HEAD "origin/$GITHUB_BASE_REF")"
|
|
127
|
+
echo "BASE_SHA=$base" >> "$GITHUB_ENV"
|
|
128
|
+
bunx checks-mutation-baseline --full "$RUNNER_TEMP/baseline-full/mutation.json"
|
|
129
|
+
bunx checks-mutation-scope "$base" "$RUNNER_TEMP/baseline-full/mutation.json" >> "$GITHUB_ENV"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Each later step reads `$SCOPE` from the environment, and a failed scope step adds nothing to it.
|
|
133
|
+
[checks-mutation-compare](checks-mutation-compare.md#running-it-in-ci) shows the whole job.
|
|
134
|
+
The full baseline comes from the `mutation` workflow, as [checks-mutation](checks-mutation.md#running-it-in-ci) shows.
|
|
135
|
+
|
|
136
|
+
## Related topics
|
|
137
|
+
|
|
138
|
+
- [checks-mutation-compare](checks-mutation-compare.md)
|
|
139
|
+
- [checks-mutation](checks-mutation.md)
|
|
140
|
+
- [checks-mutation-baseline](checks-mutation-baseline.md)
|
|
@@ -96,17 +96,20 @@ jobs:
|
|
|
96
96
|
with:
|
|
97
97
|
bun-version-file: .bun-version
|
|
98
98
|
- run: bun install --frozen-lockfile
|
|
99
|
-
- run: bunx stryker run
|
|
100
|
-
-
|
|
101
|
-
run: bunx checks-mutation-baseline --slim reports/mutation/mutation.json "$RUNNER_TEMP/report/mutation.json"
|
|
99
|
+
- run: bunx stryker run --disableBail
|
|
100
|
+
- run: bunx checks-mutation-baseline --slim reports/mutation/mutation.json "$RUNNER_TEMP/full/mutation.json"
|
|
102
101
|
- uses: actions/upload-artifact@v4
|
|
103
|
-
if: always()
|
|
104
102
|
with:
|
|
105
|
-
name: mutation-
|
|
106
|
-
path: ${{ runner.temp }}/
|
|
107
|
-
retention-days:
|
|
103
|
+
name: mutation-baseline-full
|
|
104
|
+
path: ${{ runner.temp }}/full/mutation.json
|
|
105
|
+
retention-days: 30
|
|
108
106
|
```
|
|
109
107
|
|
|
108
|
+
The run restores no state and passes `--disableBail`, so its report names every test that covers or kills each mutant.
|
|
109
|
+
The upload is the `mutation-baseline-full` artifact that a pull request's scope step restores with `checks-mutation-baseline --full`.
|
|
110
|
+
[checks-mutation-scope](checks-mutation-scope.md) reads it to bring in the sources a changed unit test covers or kills.
|
|
111
|
+
The restore reads only a successful run of `mutation.yml` on `main`, so a sweep that fails publishes nothing a pull request reads.
|
|
112
|
+
The artifact keeps 30 days, so the newest full baseline outlives the week between full runs.
|
|
110
113
|
The upload holds the report without each mutant's `statusReason`, as [checks-mutation-baseline](checks-mutation-baseline.md#the-saved-baselines) says, since the full report of a large repository passes 100 MB as an artifact.
|
|
111
114
|
|
|
112
115
|
The job runs on the fleet's self-hosted Linux runner labelled `winbox`, which is where a repository sends its full sweeps.
|
|
@@ -118,4 +121,5 @@ GitHub sets `CI=true` on every runner, so the preset lets the full run through t
|
|
|
118
121
|
## Related topics
|
|
119
122
|
|
|
120
123
|
- [checks-mutation-compare](checks-mutation-compare.md)
|
|
124
|
+
- [checks-mutation-scope](checks-mutation-scope.md)
|
|
121
125
|
- [checks-subsumed-tests](checks-subsumed-tests.md)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: reference
|
|
3
|
+
audience: consumers
|
|
4
|
+
---
|
|
5
|
+
# checks-no-plain-js
|
|
6
|
+
|
|
7
|
+
`checks-no-plain-js` is the gate that refuses a plain JavaScript source file in a repository that tracks TypeScript.
|
|
8
|
+
|
|
9
|
+
## What it checks
|
|
10
|
+
|
|
11
|
+
It fails on every `.js` or `.mjs` file in the working tree that none of the exemptions below covers.
|
|
12
|
+
Each line names the file and tells its writer to convert it to TypeScript.
|
|
13
|
+
`dist/` holds the committed build output, so the check passes over it.
|
|
14
|
+
`dependency-cruiser.config.js` at the repository root is written by `bun run build` from the TypeScript builder, so the check passes over that one path.
|
|
15
|
+
`node_modules/` and `repos/` hold installed and vendored trees, so the check passes over both.
|
|
16
|
+
`public/` holds assets the server sends verbatim, which no compiler reads, so the check passes over it.
|
|
17
|
+
`tests/fixtures/` holds data the suite never parses, so the check passes over it.
|
|
18
|
+
Stryker loads its config only as JSON or JavaScript, so the check passes over a config file carrying a Stryker config name, such as `stryker.conf.mjs`.
|
|
19
|
+
The Stryker preset ships as `stryker.preset.js` under its documented import path, so the check passes over that name.
|
|
20
|
+
A JavaScript config whose tool also loads TypeScript fails, such as a commitlint or dependency-cruiser config.
|
|
21
|
+
|
|
22
|
+
## What it reads
|
|
23
|
+
|
|
24
|
+
It reads the working tree.
|
|
25
|
+
It scans the tracked and untracked files that `git ls-files --exclude-standard` reports, so `node_modules/` and every gitignored tree are out of reach.
|
|
26
|
+
A local run agrees with CI before `git add`.
|
|
27
|
+
|
|
28
|
+
## Arguments
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
checks-no-plain-js [<directory>]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
It checks the directory it runs in, or the directory it is given.
|
|
35
|
+
|
|
36
|
+
## Exit codes
|
|
37
|
+
|
|
38
|
+
| Code | When |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| 0 | the working tree holds no plain JavaScript source |
|
|
41
|
+
| 1 | a file in the working tree is plain JavaScript |
|
|
42
|
+
| 2 | git does not list the working tree |
|
|
43
|
+
|
|
44
|
+
## Sample output
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
no-plain-js: 2 violation(s)
|
|
48
|
+
src/legacy.js: plain JavaScript source in a TypeScript repository; convert it to TypeScript
|
|
49
|
+
tools/build.mjs: plain JavaScript source in a TypeScript repository; convert it to TypeScript
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## When it runs
|
|
53
|
+
|
|
54
|
+
`checks-lint` runs it when the repository tracks TypeScript.
|
|
55
|
+
|
|
56
|
+
## Related topics
|
|
57
|
+
|
|
58
|
+
- [checks-lint](checks-lint.md)
|
|
59
|
+
- [checks-test-layout](checks-test-layout.md)
|
|
60
|
+
- [checks-unused](checks-unused.md)
|
|
@@ -20,12 +20,13 @@ It fails unless the repository holds this shape, and names the file and the path
|
|
|
20
20
|
Bun ignores both directories in the default suite.
|
|
21
21
|
A live tier with test files requires `test:live` set to `checks-test --tier=live`.
|
|
22
22
|
A pixel tier with test files requires `test:pixel` set to `checks-test --tier=pixel`.
|
|
23
|
+
Either tier script may add `--parallel=<workers>`.
|
|
23
24
|
- A test runs in-process or in a process of its own.
|
|
24
25
|
A test outside the `e2e`, `live` and `pixel` levels runs in-process, quarantined or not, so it may not import `node:child_process`, `net`, `http`, `https`, `http2`, `tls` or `dgram`.
|
|
25
26
|
It may not import `$`, `spawn`, `spawnSync`, `connect`, `serve` or `listen` from `bun`, may not touch `Bun.$` or `Bun.spawn`, and may not call `fetch`.
|
|
26
27
|
A test at the `e2e`, `live` or `pixel` level may do all of it, under `tests/quarantine/` as well.
|
|
27
28
|
Helpers in `tests/lib/**` answer to the same rule, since an in-process test reaches them.
|
|
28
|
-
- `scripts.test` is
|
|
29
|
+
- `scripts.test` is `checks-test`, optionally with `--parallel=<workers>`, which runs `bun test --randomize` as [checks-test](checks-test.md) says.
|
|
29
30
|
- `scripts.lint` runs this check, itself or through `checks-lint` called by its bare bin name.
|
|
30
31
|
- `bunfig.toml` carries every `[test]` key of the shipped preset with the same value.
|
|
31
32
|
`[test].pathIgnorePatterns` is the preset's `["**/tests/quarantine/**", "**/tests/live/**", "**/tests/pixel/**", "repos/**"]`, which the check pins itself, so the kit's own repository, whose bunfig is the preset, cannot drift it either.
|
|
@@ -78,7 +79,7 @@ It checks the directory it runs in, or the directory it is given.
|
|
|
78
79
|
```
|
|
79
80
|
test-layout: 4 violation(s)
|
|
80
81
|
src/a.test.ts: a test file must live at tests/<level>/**/*.test.ts, or tests/quarantine/<level>/**/*.test.ts while quarantined, with unit, e2e, live, or pixel as the level; move it to tests/unit/a.test.ts
|
|
81
|
-
package.json: scripts.test must be
|
|
82
|
+
package.json: scripts.test must be "checks-test", optionally with --parallel=<workers>, which runs bun test --randomize and judges its skips, found "bun test"
|
|
82
83
|
package.json: scripts.lint must run the layout check: add "checks-lint"
|
|
83
84
|
bunfig.toml: bunfig.toml is missing; bun has no bunfig extends, so copy node_modules/@avi2dg/checks/bunfig.toml
|
|
84
85
|
```
|
|
@@ -47,9 +47,11 @@ A skipped test or a todo without `skipReason` fails in every environment.
|
|
|
47
47
|
It does not read a skip list from `package.json`.
|
|
48
48
|
It counts a run as `ci` when `CI` is true and as `local` otherwise.
|
|
49
49
|
|
|
50
|
-
The command takes no arguments for the default suite.
|
|
50
|
+
The command takes no arguments for the default suite, except the worker count of [Parallel runs](#parallel-runs).
|
|
51
51
|
Use `checks-test --tier=live` or `checks-test --tier=pixel` for a named test tier.
|
|
52
|
-
|
|
52
|
+
Use `checks-test --tier=unit` or `checks-test --tier=e2e` to run only one level of the default suite.
|
|
53
|
+
A live or pixel run clears Bun's ignored paths, runs only `./tests/live` or `./tests/pixel`, and still checks each skip.
|
|
54
|
+
A unit or e2e run keeps Bun's ignored paths, runs only `./tests/unit` or `./tests/e2e`, and still checks each skip.
|
|
53
55
|
|
|
54
56
|
Files under `tests/quarantine/` are not run or judged, as [checks-test-layout](checks-test-layout.md) says.
|
|
55
57
|
|
|
@@ -72,10 +74,37 @@ Add the matching package scripts when either directory contains tests:
|
|
|
72
74
|
Bun ignores both directories during the default run.
|
|
73
75
|
`checks-test-layout` requires each tier script when its directory contains a test file.
|
|
74
76
|
|
|
77
|
+
## Parallel runs
|
|
78
|
+
|
|
79
|
+
A repository runs its test files in parallel by passing a worker count in its `test` script:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"scripts": {
|
|
84
|
+
"test": "checks-test --parallel=4"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`checks-test --parallel=<workers>` runs `bun test --randomize --parallel=<workers>`, so Bun hands each test file to one of that many worker processes.
|
|
90
|
+
Each file then runs in a fresh global, so a file cannot lean on state another file left behind.
|
|
91
|
+
The count is a whole number from 1 up, and without the option the suite runs serially in one process.
|
|
92
|
+
Tests still run in random order, and `test.only` still fails the run.
|
|
93
|
+
Bun merges every worker's results into the one JUnit report, so each skipped test and each todo is judged as in a serial run.
|
|
94
|
+
A tier script takes the same option, as in `checks-test --tier=live --parallel=4`.
|
|
95
|
+
[checks-flake](checks-flake.md#parallel-hunts) hunts with the worker count the `test` script sets.
|
|
96
|
+
|
|
97
|
+
A file whose worker crashes, for example when a test calls `process.exit`, fails the run.
|
|
98
|
+
Bun reports no result from that file, so its skips are not judged and its declarations are not reported as stale.
|
|
99
|
+
|
|
100
|
+
`checks-test` takes no timings file, because Bun ignores the order a timings file sets whenever `--randomize` is on.
|
|
101
|
+
A timings option waits for Bun to honour timings under random order.
|
|
102
|
+
|
|
75
103
|
## Arguments
|
|
76
104
|
|
|
77
|
-
The default command takes no arguments.
|
|
78
|
-
Its named tier options are `--tier=live` and `--tier=
|
|
105
|
+
The default command takes no arguments except `--parallel=<workers>`, as [Parallel runs](#parallel-runs) says.
|
|
106
|
+
Its named tier options are `--tier=live`, `--tier=pixel`, `--tier=unit` and `--tier=e2e`, and a tier run takes `--parallel=<workers>` too.
|
|
107
|
+
Each option appears at most once.
|
|
79
108
|
It refuses test filters because every test excluded by a filter would look skipped.
|
|
80
109
|
|
|
81
110
|
## Exit codes
|
|
@@ -83,8 +112,8 @@ It refuses test filters because every test excluded by a filter would look skipp
|
|
|
83
112
|
| Code | When |
|
|
84
113
|
| --- | --- |
|
|
85
114
|
| 0 | every test passed and every skip has a matching site reason |
|
|
86
|
-
| 1 | a test failed, a skip lacks a site reason or a declaration is stale in CI |
|
|
87
|
-
| 2 | a declaration cannot be read, `CI` is not a boolean or Bun wrote no report |
|
|
115
|
+
| 1 | a test failed, a parallel worker crashed, a skip lacks a site reason or a declaration is stale in CI |
|
|
116
|
+
| 2 | an argument is not one it takes, a declaration cannot be read, `CI` is not a boolean or Bun wrote no report |
|
|
88
117
|
|
|
89
118
|
## Sample output
|
|
90
119
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avi2dg/checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "Deterministic checks shared across a set of TypeScript repositories",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -17,13 +17,14 @@
|
|
|
17
17
|
"docs/design.md",
|
|
18
18
|
"docs/gates/",
|
|
19
19
|
"bunfig.toml",
|
|
20
|
-
"commitlint.config.
|
|
20
|
+
"commitlint.config.ts",
|
|
21
21
|
"dependency-cruiser.config.js",
|
|
22
22
|
"knip-base.json",
|
|
23
23
|
"src/quality/lint-coverage.sh",
|
|
24
24
|
"src/core/main.ts",
|
|
25
25
|
"src/core/git.ts",
|
|
26
26
|
"src/core/gates.ts",
|
|
27
|
+
"src/core/declared-inputs.ts",
|
|
27
28
|
"src/core/lint.ts",
|
|
28
29
|
"src/delivery/changelog.ts",
|
|
29
30
|
"src/delivery/changelog-write.ts",
|
|
@@ -39,13 +40,14 @@
|
|
|
39
40
|
"src/core/swc.ts",
|
|
40
41
|
"src/testing/test-report.ts",
|
|
41
42
|
"src/testing/flake.ts",
|
|
43
|
+
"src/testing/flake-preload.ts",
|
|
42
44
|
"src/delivery/commit-identity.ts",
|
|
43
45
|
"src/delivery/commit-title.ts",
|
|
44
46
|
"src/delivery/docs-only.ts",
|
|
45
47
|
"src/testing/mutation.ts",
|
|
46
|
-
"src/testing/mutation-
|
|
47
|
-
"src/testing/mutation-guard-plugin.js",
|
|
48
|
+
"src/testing/mutation-refusal.ts",
|
|
48
49
|
"src/testing/mutation-compare.ts",
|
|
50
|
+
"src/testing/mutation-scope.ts",
|
|
49
51
|
"src/testing/mutation-baseline.ts",
|
|
50
52
|
"src/testing/mutation-baseline-saved.ts",
|
|
51
53
|
"src/testing/subsumed-tests.ts",
|
|
@@ -71,6 +73,9 @@
|
|
|
71
73
|
"src/testing/quarantine-clock.ts",
|
|
72
74
|
"src/docs/sentence-length.ts",
|
|
73
75
|
"src/docs/docs.ts",
|
|
76
|
+
"src/docs/copy.ts",
|
|
77
|
+
"src/docs/copy-text.ts",
|
|
78
|
+
"src/docs/copy-html.ts",
|
|
74
79
|
"src/dependencies/vendor.ts",
|
|
75
80
|
"src/dependencies/vendor-args.ts",
|
|
76
81
|
"src/dependencies/cache-root.ts",
|
|
@@ -100,6 +105,7 @@
|
|
|
100
105
|
"dist/effect-channel/index.js",
|
|
101
106
|
"dist/readability/index.js",
|
|
102
107
|
"dist/data-shape/index.js",
|
|
108
|
+
"dist/mutation/",
|
|
103
109
|
"dist/presets/"
|
|
104
110
|
],
|
|
105
111
|
"exports": {
|
|
@@ -142,6 +148,7 @@
|
|
|
142
148
|
"checks-mutation": "src/testing/mutation.ts",
|
|
143
149
|
"checks-mutation-compare": "src/testing/mutation-compare.ts",
|
|
144
150
|
"checks-mutation-baseline": "src/testing/mutation-baseline.ts",
|
|
151
|
+
"checks-mutation-scope": "src/testing/mutation-scope.ts",
|
|
145
152
|
"checks-subsumed-tests": "src/testing/subsumed-tests.ts",
|
|
146
153
|
"checks-ci-wiring": "src/delivery/ci-wiring.ts",
|
|
147
154
|
"checks-comment-gate": "src/quality/comment-gate.ts",
|
|
@@ -151,20 +158,24 @@
|
|
|
151
158
|
"checks-exports": "src/complexity/exports.ts",
|
|
152
159
|
"checks-quarantine-clock": "src/testing/quarantine-clock.ts",
|
|
153
160
|
"checks-docs": "src/docs/docs.ts",
|
|
161
|
+
"checks-copy": "src/docs/copy.ts",
|
|
154
162
|
"checks-vendor": "src/dependencies/vendor.ts",
|
|
155
163
|
"checks-advisories": "src/dependencies/advisories.ts",
|
|
156
164
|
"checks-secrets": "src/delivery/secrets.ts",
|
|
157
165
|
"checks-frontend-syntax": "src/quality/frontend-syntax.ts",
|
|
166
|
+
"checks-no-plain-js": "src/quality/no-plain-js.ts",
|
|
158
167
|
"checks-browser": "src/quality/browser/browser.ts",
|
|
159
168
|
"checks-imports": "src/dependencies/imports.ts",
|
|
160
169
|
"checks-effect-scope": "src/quality/effect-scope.ts"
|
|
161
170
|
},
|
|
162
171
|
"scripts": {
|
|
163
172
|
"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",
|
|
164
|
-
"build": "bun build src/quality/effect-channel/index.ts --outdir dist/effect-channel --target node --format esm && bun build src/complexity/readability/index.ts --outdir dist/readability --target node --format esm && bun build src/quality/data-shape/index.ts --outdir dist/data-shape --target node --format esm && bun build src/quality/presets/oxlint.ts src/quality/presets/knip.ts src/quality/presets/dependency-cruiser.ts --outdir dist/presets --target node --format esm && tsc -p tsconfig.presets.json && bun scripts/configs-write.ts && bun src/quality/effect-scope.ts && bun scripts/doc-templates-write.ts && bun src/delivery/changelog-write.ts && bun scripts/doc-blocks-write.ts",
|
|
173
|
+
"build": "bun build src/quality/effect-channel/index.ts --outdir dist/effect-channel --target node --format esm && bun build src/complexity/readability/index.ts --outdir dist/readability --target node --format esm && bun build src/quality/data-shape/index.ts --outdir dist/data-shape --target node --format esm && bun build src/testing/mutation-guard-plugin.ts src/testing/mutation-refusal.ts --outdir dist/mutation --target node --format esm && bun build src/quality/presets/oxlint.ts src/quality/presets/knip.ts src/quality/presets/dependency-cruiser.ts --outdir dist/presets --target node --format esm && tsc -p tsconfig.presets.json && bun scripts/configs-write.ts && bun src/quality/effect-scope.ts && bun scripts/doc-templates-write.ts && bun src/delivery/changelog-write.ts && bun scripts/doc-blocks-write.ts",
|
|
165
174
|
"lint": "oxlint && bun src/core/lint.ts",
|
|
166
175
|
"typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",
|
|
167
176
|
"test": "bun src/testing/test.ts",
|
|
177
|
+
"test:unit": "bun src/testing/test.ts --tier=unit",
|
|
178
|
+
"test:e2e": "bun src/testing/test.ts --tier=e2e --parallel=4",
|
|
168
179
|
"mutate": "bunx stryker run",
|
|
169
180
|
"mutate:incremental": "bunx stryker run --incremental"
|
|
170
181
|
},
|
|
@@ -227,7 +238,8 @@
|
|
|
227
238
|
"@effect/platform-node-shared": "4.0.0",
|
|
228
239
|
"@total-typescript/ts-reset": "0.6.1",
|
|
229
240
|
"commonmark": "0.31.2",
|
|
230
|
-
"knip": "6.38.0"
|
|
241
|
+
"knip": "6.38.0",
|
|
242
|
+
"minimatch": "10.2.6"
|
|
231
243
|
},
|
|
232
244
|
"overrides": {
|
|
233
245
|
"qs": "6.16.0",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { BunServices } from "@effect/platform-bun";
|
|
2
|
+
import { Console, Effect, FileSystem, Path, Schema } from "effect";
|
|
3
|
+
import { runMain, Usage } from "./main.ts";
|
|
4
|
+
|
|
5
|
+
export type Declared = {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly file: string;
|
|
8
|
+
readonly declares: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type Input = {
|
|
12
|
+
readonly glob: string;
|
|
13
|
+
readonly files: readonly string[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type Violation = {
|
|
17
|
+
readonly file: string;
|
|
18
|
+
readonly line: number;
|
|
19
|
+
readonly column: number;
|
|
20
|
+
readonly text: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type Verdict<S> = {
|
|
24
|
+
readonly report: (scan: S) => string;
|
|
25
|
+
readonly passes: (scan: S) => boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const Declaration = Schema.Struct({ inputs: Schema.NonEmptyArray(Schema.NonEmptyString) });
|
|
29
|
+
|
|
30
|
+
const decodeDeclaration = Schema.decodeUnknownEffect(Schema.fromJsonString(Declaration));
|
|
31
|
+
|
|
32
|
+
const readInputs = Effect.fn("readInputs")(function* (root: string, { file, declares }: Declared) {
|
|
33
|
+
const fs = yield* FileSystem.FileSystem;
|
|
34
|
+
const path = (yield* Path.Path).join(root, file);
|
|
35
|
+
if (!(yield* fs.exists(path))) {
|
|
36
|
+
return yield* new Usage({ message: `${file} is missing, and a repository opts in to this gate by declaring its ${declares} there` });
|
|
37
|
+
}
|
|
38
|
+
const { inputs } = yield* decodeDeclaration(yield* fs.readFileString(path)).pipe(
|
|
39
|
+
Effect.mapError(({ message }) => new Usage({ message: `${file} does not decode: ${message}` })),
|
|
40
|
+
);
|
|
41
|
+
return inputs;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export function emptyInputs(inputs: readonly Input[]): readonly Input[] {
|
|
45
|
+
return inputs.filter(({ files }) => files.length === 0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function violationLine({ file, line, column, text }: Violation): string {
|
|
49
|
+
return `${file}:${line}:${column} ${text}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function inputsReport(name: string, inputs: readonly Input[], problems: readonly string[]): string {
|
|
53
|
+
const files = new Set(inputs.flatMap((input) => input.files)).size;
|
|
54
|
+
const scanned = `${files} file(s) from ${inputs.length} declared input(s)`;
|
|
55
|
+
if (problems.length === 0) return `${name}: no violation in ${scanned}`;
|
|
56
|
+
return [`${name}: ${problems.length} problem(s) in ${scanned}:`, ...problems.map((problem) => ` ${problem}`)].join("\n");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const declaredGate = Effect.fn("declaredGate")(function* <S, E>(
|
|
60
|
+
declared: Declared,
|
|
61
|
+
scanInputs: (root: string, globs: readonly string[]) => Effect.Effect<S, E, BunServices.BunServices>,
|
|
62
|
+
{ report, passes }: Verdict<S>,
|
|
63
|
+
) {
|
|
64
|
+
const root = (yield* Path.Path).resolve(process.argv[2] ?? ".");
|
|
65
|
+
const scan = yield* scanInputs(root, yield* readInputs(root, declared));
|
|
66
|
+
yield* Console.log(report(scan));
|
|
67
|
+
return passes(scan);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export function runDeclaredGate<S, E extends { readonly message: string }>(
|
|
71
|
+
declared: Declared,
|
|
72
|
+
scanInputs: (root: string, globs: readonly string[]) => Effect.Effect<S, E, BunServices.BunServices>,
|
|
73
|
+
verdict: Verdict<S>,
|
|
74
|
+
): void {
|
|
75
|
+
runMain(declared.name, declaredGate(declared, scanInputs, verdict));
|
|
76
|
+
}
|
package/src/core/gates.ts
CHANGED
|
@@ -40,6 +40,8 @@ const OXLINT_CONFIG: TrackedContent = { pathspecs: ["oxlint.config.ts"], content
|
|
|
40
40
|
|
|
41
41
|
const FRONTEND_SYNTAX_DECLARATION: TrackedContent = { pathspecs: ["frontend-syntax.json"], content: "a frontend syntax declaration" };
|
|
42
42
|
|
|
43
|
+
const COPY_DECLARATION: TrackedContent = { pathspecs: ["copy.json"], content: "a copy declaration" };
|
|
44
|
+
|
|
43
45
|
export const KIT_GATES = [
|
|
44
46
|
{ bin: "checks-lint-coverage", vector: "quality", file: "lint-coverage.sh", reads: "tree", appliesTo: LINTED_SOURCE },
|
|
45
47
|
{ bin: "checks-test-layout", vector: "testing", file: "test-layout.ts", reads: "tree", appliesTo: TYPESCRIPT_SOURCE },
|
|
@@ -47,10 +49,12 @@ export const KIT_GATES = [
|
|
|
47
49
|
{ bin: "checks-comment-gate", vector: "quality", file: "comment-gate.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
48
50
|
{ bin: "checks-effect-scope", vector: "quality", file: "effect-scope.ts", reads: "tree", args: ["--check"], appliesTo: OXLINT_CONFIG },
|
|
49
51
|
{ bin: "checks-frontend-syntax", vector: "quality", file: "frontend-syntax.ts", reads: "tree", appliesTo: FRONTEND_SYNTAX_DECLARATION },
|
|
52
|
+
{ bin: "checks-no-plain-js", vector: "quality", file: "no-plain-js.ts", reads: "tree", appliesTo: TYPESCRIPT_SOURCE },
|
|
50
53
|
{ bin: "checks-suppressions-ratchet", vector: "complexity", file: "suppressions-ratchet.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
51
54
|
{ bin: "checks-ci-wiring", vector: "delivery", file: "ci-wiring.ts", reads: "tree", appliesTo: EVERY_REPOSITORY },
|
|
52
55
|
{ bin: "checks-secrets", vector: "delivery", file: "secrets.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
53
56
|
{ bin: "checks-docs", vector: "docs", file: "docs.ts", reads: "range", alsoReads: "every agent file at the head commit", appliesTo: EVERY_REPOSITORY },
|
|
57
|
+
{ bin: "checks-copy", vector: "docs", file: "copy.ts", reads: "tree", appliesTo: COPY_DECLARATION },
|
|
54
58
|
{ bin: "checks-repetition", vector: "complexity", file: "repetition.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
|
|
55
59
|
{ bin: "checks-unused", vector: "complexity", file: "unused.ts", reads: "tree", appliesTo: LINTED_SOURCE },
|
|
56
60
|
{ bin: "checks-exports", vector: "complexity", file: "exports.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
|
package/src/core/git.ts
CHANGED
|
@@ -29,6 +29,15 @@ export const collect = Effect.fn("collect")(
|
|
|
29
29
|
Effect.scoped,
|
|
30
30
|
);
|
|
31
31
|
|
|
32
|
+
export const workingTreeFiles = Effect.fn("workingTreeFiles")(function* (root: string) {
|
|
33
|
+
const fs = yield* FileSystem.FileSystem;
|
|
34
|
+
const path = yield* Path.Path;
|
|
35
|
+
const listed = yield* git(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], root);
|
|
36
|
+
return yield* Effect.filter(listed.split("\0").filter(Boolean), (file) => fs.exists(path.join(root, file)), {
|
|
37
|
+
concurrency: "unbounded",
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
32
41
|
export const git = Effect.fn("git")(function* (args: readonly string[], cwd?: string, feed?: Feed) {
|
|
33
42
|
const failed = (reason: string): GitFailure => new GitFailure({ message: `git ${args.join(" ")}: ${reason.trim()}` });
|
|
34
43
|
const { stdout, stderr, exitCode } = yield* collect("git", args, cwd, feed).pipe(
|
|
@@ -17,6 +17,7 @@ export type Gate = {
|
|
|
17
17
|
readonly command: string;
|
|
18
18
|
readonly words: Command;
|
|
19
19
|
readonly alternatives: readonly Command[];
|
|
20
|
+
readonly partition: readonly Command[];
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
export type Declaration = {
|
|
@@ -63,6 +64,8 @@ export class WiringError extends Schema.TaggedError<WiringError>()("WiringError"
|
|
|
63
64
|
|
|
64
65
|
const WORKFLOWS = ".github/workflows";
|
|
65
66
|
const SCRIPTED = ["lint", "build", "typecheck", "test"];
|
|
67
|
+
const WHOLE_SUITE = "bun run test";
|
|
68
|
+
const SPLIT_SUITE = ["test:unit", "test:e2e"];
|
|
66
69
|
const BUILT_TREE = "git diff --exit-code";
|
|
67
70
|
const TITLE_LINT = "bunx checks-commit-title";
|
|
68
71
|
// The gate holds until every consumer migrates, so the direct commitlint call still counts.
|
|
@@ -196,6 +199,21 @@ function entryPointCommand(gate: Command, lintGates: readonly KitGate[]): Comman
|
|
|
196
199
|
return index === -1 ? undefined : [...gate.slice(0, index), ENTRY_POINT.bin];
|
|
197
200
|
}
|
|
198
201
|
|
|
202
|
+
type Invocation = Omit<RunStep, "script">;
|
|
203
|
+
|
|
204
|
+
function invocations(commands: readonly { command: string; words: Command }[], steps: readonly RunStep[]): readonly Invocation[] {
|
|
205
|
+
return steps.flatMap(({ location, blocker, script }) => {
|
|
206
|
+
const plain = plainCommand(script);
|
|
207
|
+
if (commands.some(({ words }) => invokes(plain, words))) return [{ location, blocker }];
|
|
208
|
+
const mentioned = commands.find(({ words }) => mentions(script, words));
|
|
209
|
+
if (mentioned === undefined) return [];
|
|
210
|
+
const alone = `the step runs more than ${mentioned.command}; give it its own step with nothing else in it`;
|
|
211
|
+
return [{ location, blocker: blocker ?? alone }];
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const runsUnblocked = (invoking: readonly Invocation[]): boolean => invoking.some((step) => step.blocker === undefined);
|
|
216
|
+
|
|
199
217
|
function gapsAmong(gates: readonly Gate[], steps: readonly RunStep[], lintGates: readonly KitGate[]): readonly Gap[] {
|
|
200
218
|
return gates.flatMap((gate) => {
|
|
201
219
|
const entryPoint = entryPointCommand(gate.words, lintGates);
|
|
@@ -204,16 +222,10 @@ function gapsAmong(gates: readonly Gate[], steps: readonly RunStep[], lintGates:
|
|
|
204
222
|
...gate.alternatives.map((words) => ({ command: gate.command, words })),
|
|
205
223
|
...(entryPoint === undefined ? [] : [{ command: entryPoint.join(" "), words: entryPoint }]),
|
|
206
224
|
];
|
|
207
|
-
const invoking =
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (mentioned === undefined) return [];
|
|
212
|
-
const alone = `the step runs more than ${mentioned.command}; give it its own step with nothing else in it`;
|
|
213
|
-
return [{ location, blocker: blocker ?? alone }];
|
|
214
|
-
});
|
|
215
|
-
if (invoking.some((step) => step.blocker === undefined)) return [];
|
|
216
|
-
const blocked = invoking.flatMap(({ location, blocker }) =>
|
|
225
|
+
const invoking = invocations(commands, steps);
|
|
226
|
+
const parts = gate.partition.map((words) => invocations([{ command: words.join(" "), words }], steps));
|
|
227
|
+
if (runsUnblocked(invoking) || (parts.length > 0 && parts.every(runsUnblocked))) return [];
|
|
228
|
+
const blocked = [...invoking, ...parts.flat()].flatMap(({ location, blocker }) =>
|
|
217
229
|
blocker === undefined ? [] : [{ location, blocker }],
|
|
218
230
|
);
|
|
219
231
|
return [{ gate: gate.command, ...(entryPoint === undefined ? {} : { entryPoint: entryPoint.join(" ") }), blocked }];
|
|
@@ -252,11 +264,13 @@ export function requiredCommands(scripts: readonly string[]): readonly string[]
|
|
|
252
264
|
}
|
|
253
265
|
|
|
254
266
|
export function declarationFor(scripts: readonly string[], branch: string): Declaration {
|
|
267
|
+
const tiers = SPLIT_SUITE.every((name) => scripts.includes(name)) ? SPLIT_SUITE.map((name) => ["bun", "run", name]) : [];
|
|
255
268
|
return {
|
|
256
269
|
gates: requiredCommands(scripts).map((command) => ({
|
|
257
270
|
command,
|
|
258
271
|
words: command.split(" "),
|
|
259
272
|
alternatives: command === TITLE_LINT ? TITLE_CALLS.map((call) => call.split(" ")) : [],
|
|
273
|
+
partition: command === WHOLE_SUITE ? tiers : [],
|
|
260
274
|
})),
|
|
261
275
|
defaultBranch: branch,
|
|
262
276
|
lintGates: KIT_GATES,
|
|
@@ -277,7 +291,7 @@ function readsOverride(runsOn: unknown): boolean {
|
|
|
277
291
|
function runnerFault(runsOn: unknown, mutation: boolean, visibility: Visibility): string | undefined {
|
|
278
292
|
const overridden = readsOverride(runsOn);
|
|
279
293
|
const pin = `set runs-on: [${MUTATION_RUNNER.join(", ")}]`;
|
|
280
|
-
if (mutation && overridden) return `a mutation job reads CI_RUNS_ON, so an override moves its
|
|
294
|
+
if (mutation && overridden) return `a mutation job reads CI_RUNS_ON, so an override moves its mutation runs off winbox; ${pin}`;
|
|
281
295
|
if (mutation) return visibility === "private" && !sameLabels(runsOn, MUTATION_RUNNER) ? `a mutation job in a private repository runs off winbox; ${pin}` : undefined;
|
|
282
296
|
const hosted = typeof runsOn === "string" && HOSTED_OVERRIDE.test(runsOn.trim());
|
|
283
297
|
if (hosted || (visibility !== "private" && !overridden)) return undefined;
|
|
@@ -23,8 +23,8 @@ const cliOf = Effect.fn("cliOf")(function* () {
|
|
|
23
23
|
// The config ships with the kit, so it sits two levels above this file wherever the kit is installed.
|
|
24
24
|
const configOf = Effect.fn("configOf")(function* () {
|
|
25
25
|
const file = yield* Effect.try({
|
|
26
|
-
try: () => new URL("../../commitlint.config.
|
|
27
|
-
catch: () => setup("cannot resolve commitlint.config.
|
|
26
|
+
try: () => new URL("../../commitlint.config.ts", import.meta.url),
|
|
27
|
+
catch: () => setup("cannot resolve commitlint.config.ts from the installed kit"),
|
|
28
28
|
});
|
|
29
29
|
return yield* (yield* Path.Path).fromFileUrl(file);
|
|
30
30
|
});
|