coverage-check 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -51
- package/package.json +2 -1
- package/src/commands/check-args.mts +110 -0
- package/src/commands/check.mts +41 -94
- package/src/commands/check.test.mts +253 -25
- package/src/commands/store-put.mts +37 -22
- package/src/commands/store-put.test.mts +117 -23
- package/src/coverage-check.mts +2 -0
- package/src/github-comment.mts +8 -3
- package/src/github-comment.test.mts +4 -5
- package/src/report.mts +0 -9
- package/src/report.test.mts +1 -18
- package/src/s3-suite-store.mts +138 -0
- package/src/s3-suite-store.test.mts +308 -0
- package/src/step-summary.mts +89 -0
- package/src/step-summary.test.mts +189 -0
- package/src/store-factory.mts +23 -0
- package/src/store-factory.test.mts +67 -0
- package/src/suite-store.mts +67 -17
- package/src/suite-store.test.mts +124 -30
- package/src/types.mts +1 -1
package/README.md
CHANGED
|
@@ -22,46 +22,66 @@ coverage-check check \
|
|
|
22
22
|
|
|
23
23
|
Exits `0` on pass, `1` on failure, `2` on configuration error.
|
|
24
24
|
|
|
25
|
-
### Suite store (conditional CI)
|
|
25
|
+
### Suite store with S3 (conditional CI)
|
|
26
26
|
|
|
27
|
-
When only some CI suites run per PR (e.g. backend tests only when backend files change), store each suite's LCOV
|
|
27
|
+
When only some CI suites run per PR (e.g. backend tests only when backend files change), store each suite's LCOV in S3 and merge them during coverage checks:
|
|
28
28
|
|
|
29
29
|
```sh
|
|
30
|
-
# After backend tests run — store this suite's coverage
|
|
30
|
+
# After backend tests run on the main branch — store this suite's coverage
|
|
31
31
|
coverage-check store-put \
|
|
32
32
|
--suite backend \
|
|
33
|
-
--store
|
|
33
|
+
--store-s3 my-bucket/coverage-store \
|
|
34
34
|
--artifacts ./coverage-artifacts \
|
|
35
35
|
--sha "$GITHUB_SHA" \
|
|
36
|
-
--
|
|
36
|
+
--branch main
|
|
37
37
|
|
|
38
|
-
#
|
|
39
|
-
|
|
38
|
+
# On a PR that only runs frontend tests:
|
|
39
|
+
coverage-check check \
|
|
40
|
+
--rules .coverage-rules.yml \
|
|
41
|
+
--artifacts ./coverage-artifacts \
|
|
42
|
+
--store-s3 my-bucket/coverage-store \
|
|
43
|
+
--suite frontend \
|
|
44
|
+
--branch main \
|
|
45
|
+
--base origin/main \
|
|
46
|
+
--head HEAD
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The `--suite` flag on `check` tells the tool to use fresh `--artifacts` for the current suite and pull historical coverage from the store for all other suites. The `--branch` flag selects which branch pointer to follow when reading from the store.
|
|
50
|
+
|
|
51
|
+
**S3 key layout:**
|
|
40
52
|
|
|
41
|
-
|
|
53
|
+
```text
|
|
54
|
+
<prefix>/<suite>/sha/<sha>/lcov.info # payload
|
|
55
|
+
<prefix>/<suite>/branch/<branch>/latest.json # pointer: { "sha": "...", "timestamp": "..." }
|
|
56
|
+
```
|
|
42
57
|
|
|
43
|
-
|
|
44
|
-
aws s3 sync s3://my-bucket/coverage-store/ ./coverage-store
|
|
58
|
+
### Suite store with filesystem
|
|
45
59
|
|
|
46
|
-
|
|
47
|
-
|
|
60
|
+
For local development or simpler deployments:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
coverage-check store-put \
|
|
64
|
+
--suite backend \
|
|
65
|
+
--store-fs ./coverage-store \
|
|
66
|
+
--artifacts ./coverage-artifacts \
|
|
67
|
+
--sha "$GITHUB_SHA" \
|
|
68
|
+
--branch main
|
|
48
69
|
|
|
49
|
-
# Check: merges all stored suites (backend from baseline + frontend from this run)
|
|
50
70
|
coverage-check check \
|
|
51
71
|
--rules .coverage-rules.yml \
|
|
52
72
|
--artifacts ./coverage-artifacts \
|
|
53
|
-
--store ./coverage-store \
|
|
73
|
+
--store-fs ./coverage-store \
|
|
54
74
|
--suite frontend \
|
|
55
75
|
--base origin/main \
|
|
56
76
|
--head HEAD
|
|
57
77
|
```
|
|
58
78
|
|
|
59
|
-
The `--suite` flag on `check` tells the tool to replace the same-named suite in the store with the fresh `--artifacts` (so you always see this PR's coverage for the suite that ran, and historical coverage for suites that didn't).
|
|
60
|
-
|
|
61
79
|
### GitHub PR sticky comment
|
|
62
80
|
|
|
63
81
|
Pass `--pr` and `--repo` to post (or update) a sticky comment on a pull request. Requires the `gh` CLI and `GH_TOKEN`/`GITHUB_TOKEN`.
|
|
64
82
|
|
|
83
|
+
On **failure**, the comment is created or updated with the list of uncovered lines. On **pass**, any existing failure comment is **deleted** — no new comment is posted.
|
|
84
|
+
|
|
65
85
|
```sh
|
|
66
86
|
coverage-check check \
|
|
67
87
|
--rules .coverage-rules.yml \
|
|
@@ -70,6 +90,10 @@ coverage-check check \
|
|
|
70
90
|
--repo "${{ github.repository }}"
|
|
71
91
|
```
|
|
72
92
|
|
|
93
|
+
### GitHub Actions step summary
|
|
94
|
+
|
|
95
|
+
When `$GITHUB_STEP_SUMMARY` is set, a per-suite totals and per-rule patch-coverage table is appended to the job summary automatically.
|
|
96
|
+
|
|
73
97
|
## Rules file
|
|
74
98
|
|
|
75
99
|
```yaml
|
|
@@ -89,49 +113,47 @@ Rules are matched in order; the first match wins. Files in the diff not matched
|
|
|
89
113
|
|
|
90
114
|
### `coverage-check check`
|
|
91
115
|
|
|
92
|
-
| Flag | Default | Description
|
|
93
|
-
| ---------------- | ---------------------- |
|
|
94
|
-
| `--rules` | `.coverage-rules.yml` | Path to YAML rules file
|
|
95
|
-
| `--artifacts` | `./coverage-artifacts` | Directory to scan for `lcov.info` files
|
|
96
|
-
| `--base` | `origin/main` | Base git ref for `git diff`
|
|
97
|
-
| `--head` | `HEAD` | Head git ref for `git diff`
|
|
98
|
-
| `--store`
|
|
99
|
-
| `--
|
|
100
|
-
| `--
|
|
101
|
-
| `--
|
|
102
|
-
| `--
|
|
103
|
-
| `--
|
|
116
|
+
| Flag | Default | Description |
|
|
117
|
+
| ---------------- | ---------------------- | -------------------------------------------------------------------------------------------- |
|
|
118
|
+
| `--rules` | `.coverage-rules.yml` | Path to YAML rules file |
|
|
119
|
+
| `--artifacts` | `./coverage-artifacts` | Directory to scan for `lcov.info` files |
|
|
120
|
+
| `--base` | `origin/main` | Base git ref for `git diff` |
|
|
121
|
+
| `--head` | `HEAD` | Head git ref for `git diff` |
|
|
122
|
+
| `--store-fs` | — | Path to a filesystem suite store directory |
|
|
123
|
+
| `--store` | — | Alias for `--store-fs` |
|
|
124
|
+
| `--store-s3` | — | S3 suite store spec: `<bucket>[/<prefix>]` |
|
|
125
|
+
| `--branch` | `"main"` | Branch pointer to follow when reading from the store (no `/` or `\\`) |
|
|
126
|
+
| `--suite` | — | Name of the current suite (no `/` or `\\`); fresh artifacts override this suite in the store |
|
|
127
|
+
| `--strip-prefix` | — | Extra path prefix to strip from LCOV `SF:` lines (repeatable) |
|
|
128
|
+
| `--pr` | — | Pull request number for sticky comment |
|
|
129
|
+
| `--repo` | `$GITHUB_REPOSITORY` | `owner/repo` for sticky comment |
|
|
130
|
+
| `--json` | — | Write JSON result to this path |
|
|
104
131
|
|
|
105
132
|
### `coverage-check store-put`
|
|
106
133
|
|
|
107
|
-
| Flag | Default | Description
|
|
108
|
-
| ---------------- | ---------------------- |
|
|
109
|
-
| `--suite` | required | Suite name to store
|
|
110
|
-
| `--store`
|
|
111
|
-
| `--
|
|
112
|
-
| `--
|
|
113
|
-
| `--sha` |
|
|
114
|
-
| `--
|
|
134
|
+
| Flag | Default | Description |
|
|
135
|
+
| ---------------- | ---------------------- | ------------------------------------------------------------- |
|
|
136
|
+
| `--suite` | required | Suite name to store |
|
|
137
|
+
| `--store-fs` | required\* | Path to a filesystem suite store directory |
|
|
138
|
+
| `--store` | — | Alias for `--store-fs` |
|
|
139
|
+
| `--store-s3` | required\* | S3 suite store spec: `<bucket>[/<prefix>]` |
|
|
140
|
+
| `--sha` | required | Git SHA to associate with this coverage payload |
|
|
141
|
+
| `--branch` | required | Branch name for the pointer (e.g. `main`) |
|
|
142
|
+
| `--artifacts` | `./coverage-artifacts` | Directory to scan for `lcov.info` files |
|
|
143
|
+
| `--strip-prefix` | — | Extra path prefix to strip from LCOV `SF:` lines (repeatable) |
|
|
144
|
+
|
|
145
|
+
\* Exactly one of `--store-fs` or `--store-s3` is required.
|
|
115
146
|
|
|
116
147
|
## Programmatic API
|
|
117
148
|
|
|
118
149
|
```ts
|
|
119
|
-
import { runCheck, runStorePut, FileSystemSuiteStore } from "coverage-check";
|
|
150
|
+
import { runCheck, runStorePut, FileSystemSuiteStore, S3SuiteStore } from "coverage-check";
|
|
120
151
|
|
|
121
|
-
//
|
|
122
|
-
|
|
152
|
+
// FileSystem store
|
|
153
|
+
const fsStore = new FileSystemSuiteStore("/path/to/store");
|
|
123
154
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
/* ... */
|
|
127
|
-
}
|
|
128
|
-
async get(suite: string) {
|
|
129
|
-
/* ... */
|
|
130
|
-
}
|
|
131
|
-
async put(suite: string, lcov: Buffer, meta?: SuiteMeta) {
|
|
132
|
-
/* ... */
|
|
133
|
-
}
|
|
134
|
-
}
|
|
155
|
+
// S3 store (requires AWS credentials — see https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html)
|
|
156
|
+
const s3Store = new S3SuiteStore({ bucket: "my-bucket", prefix: "coverage" });
|
|
135
157
|
|
|
136
158
|
await runCheck({
|
|
137
159
|
rules: ".coverage-rules.yml",
|
|
@@ -142,9 +164,41 @@ await runCheck({
|
|
|
142
164
|
repo: "",
|
|
143
165
|
json: null,
|
|
144
166
|
stripPrefixes: [],
|
|
145
|
-
store:
|
|
167
|
+
store: s3Store,
|
|
146
168
|
suite: "backend",
|
|
169
|
+
branch: "main",
|
|
147
170
|
});
|
|
171
|
+
|
|
172
|
+
await runStorePut({
|
|
173
|
+
suite: "backend",
|
|
174
|
+
store: s3Store,
|
|
175
|
+
artifacts: "./coverage",
|
|
176
|
+
stripPrefixes: [],
|
|
177
|
+
sha: "abc123",
|
|
178
|
+
branch: "main",
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
You can also implement your own `SuiteStore`:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import type { SuiteStore } from "coverage-check";
|
|
186
|
+
|
|
187
|
+
class MyCustomStore implements SuiteStore {
|
|
188
|
+
async list(): Promise<string[]> {
|
|
189
|
+
/* ... */
|
|
190
|
+
}
|
|
191
|
+
async get(suite: string, opts?: { sha?: string; branch?: string }): Promise<Buffer | null> {
|
|
192
|
+
/* ... */
|
|
193
|
+
}
|
|
194
|
+
async put(
|
|
195
|
+
suite: string,
|
|
196
|
+
lcov: Buffer,
|
|
197
|
+
meta: { sha: string; branch: string; timestamp?: string },
|
|
198
|
+
): Promise<void> {
|
|
199
|
+
/* ... */
|
|
200
|
+
}
|
|
201
|
+
}
|
|
148
202
|
```
|
|
149
203
|
|
|
150
204
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coverage-check",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Patch-coverage gate: checks that newly added lines meet per-path coverage thresholds. Supports per-suite LCOV accumulation for conditional CI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jonathan Ong",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"node": ">=22.0.0"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
+
"@aws-sdk/client-s3": "^3.1048.0",
|
|
21
22
|
"js-yaml": "^4.1.1"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { makeStore } from "../store-factory.mts";
|
|
2
|
+
import { assertSafePathComponent } from "../suite-store.mts";
|
|
3
|
+
import type { SuiteStore } from "../suite-store.mts";
|
|
4
|
+
import type { GhRunner } from "../github-comment.mts";
|
|
5
|
+
|
|
6
|
+
export type CheckArgs = {
|
|
7
|
+
rules: string;
|
|
8
|
+
artifacts: string;
|
|
9
|
+
base: string;
|
|
10
|
+
head: string;
|
|
11
|
+
pr: number | null;
|
|
12
|
+
repo: string;
|
|
13
|
+
json: string | null;
|
|
14
|
+
stripPrefixes: string[];
|
|
15
|
+
store: SuiteStore | null;
|
|
16
|
+
suite: string | null;
|
|
17
|
+
/** Branch used to resolve baseline from the store. Default: "main". */
|
|
18
|
+
branch?: string;
|
|
19
|
+
gh?: GhRunner;
|
|
20
|
+
/** Path to append the GitHub step summary. Default: $GITHUB_STEP_SUMMARY. */
|
|
21
|
+
summaryFile?: string | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function parseCheckArgs(argv: string[]): CheckArgs {
|
|
25
|
+
let storeFs: string | null = null;
|
|
26
|
+
let storeS3: string | null = null;
|
|
27
|
+
const args: Omit<CheckArgs, "store"> & { store: SuiteStore | null } = {
|
|
28
|
+
rules: ".coverage-rules.yml",
|
|
29
|
+
artifacts: "./coverage-artifacts",
|
|
30
|
+
base: "origin/main",
|
|
31
|
+
head: "HEAD",
|
|
32
|
+
pr: null,
|
|
33
|
+
repo: process.env["GITHUB_REPOSITORY"] ?? "",
|
|
34
|
+
json: null,
|
|
35
|
+
stripPrefixes: [],
|
|
36
|
+
store: null,
|
|
37
|
+
suite: null,
|
|
38
|
+
branch: "main",
|
|
39
|
+
summaryFile: process.env["GITHUB_STEP_SUMMARY"] ?? null,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const flag = argv[i]!;
|
|
44
|
+
const next = argv[i + 1];
|
|
45
|
+
const val = (): string => {
|
|
46
|
+
if (next === undefined || next.startsWith("--")) {
|
|
47
|
+
throw new Error(`${flag} requires a value`);
|
|
48
|
+
}
|
|
49
|
+
i++;
|
|
50
|
+
return next;
|
|
51
|
+
};
|
|
52
|
+
switch (flag) {
|
|
53
|
+
case "--rules":
|
|
54
|
+
args.rules = val();
|
|
55
|
+
break;
|
|
56
|
+
case "--artifacts":
|
|
57
|
+
args.artifacts = val();
|
|
58
|
+
break;
|
|
59
|
+
case "--base":
|
|
60
|
+
args.base = val();
|
|
61
|
+
break;
|
|
62
|
+
case "--head":
|
|
63
|
+
args.head = val();
|
|
64
|
+
break;
|
|
65
|
+
case "--repo":
|
|
66
|
+
args.repo = val();
|
|
67
|
+
break;
|
|
68
|
+
case "--json":
|
|
69
|
+
args.json = val();
|
|
70
|
+
break;
|
|
71
|
+
case "--suite": {
|
|
72
|
+
const s = val();
|
|
73
|
+
assertSafePathComponent(s, "suite");
|
|
74
|
+
args.suite = s;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
case "--strip-prefix":
|
|
78
|
+
args.stripPrefixes.push(val());
|
|
79
|
+
break;
|
|
80
|
+
case "--branch": {
|
|
81
|
+
const b = val();
|
|
82
|
+
assertSafePathComponent(b, "branch");
|
|
83
|
+
args.branch = b;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case "--store":
|
|
87
|
+
case "--store-fs":
|
|
88
|
+
storeFs = val();
|
|
89
|
+
break;
|
|
90
|
+
case "--store-s3":
|
|
91
|
+
storeS3 = val();
|
|
92
|
+
break;
|
|
93
|
+
case "--pr": {
|
|
94
|
+
const raw = val();
|
|
95
|
+
if (!/^\d+$/.test(raw) || raw === "0")
|
|
96
|
+
throw new Error(`--pr must be a positive integer, got: ${JSON.stringify(raw)}`);
|
|
97
|
+
args.pr = parseInt(raw, 10);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
default:
|
|
101
|
+
throw new Error(`unknown flag: ${flag}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (storeFs && storeS3) throw new Error("--store-fs and --store-s3 are mutually exclusive");
|
|
106
|
+
if (args.pr !== null && args.repo.trim() === "")
|
|
107
|
+
throw new Error("--repo is required when --pr is set (or define GITHUB_REPOSITORY)");
|
|
108
|
+
args.store = makeStore({ fs: storeFs, s3: storeS3 });
|
|
109
|
+
return args;
|
|
110
|
+
}
|
package/src/commands/check.mts
CHANGED
|
@@ -4,103 +4,25 @@ import { mergeLcov } from "../lcov-merge.mts";
|
|
|
4
4
|
import { getChangedLines } from "../diff-parser.mts";
|
|
5
5
|
import { loadRules } from "../rules.mts";
|
|
6
6
|
import { computePatchCoverage } from "../patch-coverage.mts";
|
|
7
|
-
import { collapseRanges, renderFailureComment
|
|
7
|
+
import { collapseRanges, renderFailureComment } from "../report.mts";
|
|
8
8
|
import { upsertComment } from "../github-comment.mts";
|
|
9
9
|
import { collectLcovFiles, buildStripPrefixes } from "../load-artifacts.mts";
|
|
10
|
-
import {
|
|
10
|
+
import { writeSummary } from "../step-summary.mts";
|
|
11
|
+
import { parseCheckArgs } from "./check-args.mts";
|
|
12
|
+
import type { CheckArgs } from "./check-args.mts";
|
|
13
|
+
import type { SuiteSource } from "../step-summary.mts";
|
|
11
14
|
import type { LcovData } from "../types.mts";
|
|
12
|
-
|
|
13
|
-
import type { GhRunner } from "../github-comment.mts";
|
|
15
|
+
export type { CheckArgs } from "./check-args.mts";
|
|
14
16
|
|
|
15
17
|
const stdout = (msg: string) => process.stdout.write(`${msg}\n`);
|
|
16
18
|
const stderr = (msg: string) => process.stderr.write(`${msg}\n`);
|
|
17
19
|
|
|
18
|
-
export type CheckArgs = {
|
|
19
|
-
rules: string;
|
|
20
|
-
artifacts: string;
|
|
21
|
-
base: string;
|
|
22
|
-
head: string;
|
|
23
|
-
pr: number | null;
|
|
24
|
-
repo: string;
|
|
25
|
-
json: string | null;
|
|
26
|
-
stripPrefixes: string[];
|
|
27
|
-
store: SuiteStore | null;
|
|
28
|
-
suite: string | null;
|
|
29
|
-
gh?: GhRunner;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
function parseArgs(argv: string[]): CheckArgs {
|
|
33
|
-
const args: CheckArgs = {
|
|
34
|
-
rules: ".coverage-rules.yml",
|
|
35
|
-
artifacts: "./coverage-artifacts",
|
|
36
|
-
base: "origin/main",
|
|
37
|
-
head: "HEAD",
|
|
38
|
-
pr: null,
|
|
39
|
-
repo: process.env["GITHUB_REPOSITORY"] ?? "",
|
|
40
|
-
json: null,
|
|
41
|
-
stripPrefixes: [],
|
|
42
|
-
store: null,
|
|
43
|
-
suite: null,
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
for (let i = 0; i < argv.length; i++) {
|
|
47
|
-
const flag = argv[i]!;
|
|
48
|
-
const next = argv[i + 1];
|
|
49
|
-
const val = (): string => {
|
|
50
|
-
if (next === undefined) throw new Error(`${flag} requires a value`);
|
|
51
|
-
i++;
|
|
52
|
-
return next;
|
|
53
|
-
};
|
|
54
|
-
switch (flag) {
|
|
55
|
-
case "--rules":
|
|
56
|
-
args.rules = val();
|
|
57
|
-
break;
|
|
58
|
-
case "--artifacts":
|
|
59
|
-
args.artifacts = val();
|
|
60
|
-
break;
|
|
61
|
-
case "--base":
|
|
62
|
-
args.base = val();
|
|
63
|
-
break;
|
|
64
|
-
case "--head":
|
|
65
|
-
args.head = val();
|
|
66
|
-
break;
|
|
67
|
-
case "--pr": {
|
|
68
|
-
const raw = val();
|
|
69
|
-
if (!/^\d+$/.test(raw) || raw === "0")
|
|
70
|
-
throw new Error(`--pr must be a positive integer, got: ${JSON.stringify(raw)}`);
|
|
71
|
-
args.pr = parseInt(raw, 10);
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
case "--repo":
|
|
75
|
-
args.repo = val();
|
|
76
|
-
break;
|
|
77
|
-
case "--json":
|
|
78
|
-
args.json = val();
|
|
79
|
-
break;
|
|
80
|
-
case "--strip-prefix":
|
|
81
|
-
args.stripPrefixes.push(val());
|
|
82
|
-
break;
|
|
83
|
-
case "--store":
|
|
84
|
-
args.store = new FileSystemSuiteStore(val());
|
|
85
|
-
break;
|
|
86
|
-
case "--suite":
|
|
87
|
-
args.suite = val();
|
|
88
|
-
break;
|
|
89
|
-
default:
|
|
90
|
-
throw new Error(`unknown flag: ${flag}`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return args;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
20
|
export async function main(argv: string[]): Promise<number> {
|
|
98
21
|
let args: CheckArgs;
|
|
99
22
|
try {
|
|
100
|
-
args =
|
|
23
|
+
args = parseCheckArgs(argv);
|
|
101
24
|
} catch (err) {
|
|
102
|
-
|
|
103
|
-
stderr(`coverage-check: ${err instanceof Error ? err.message : err}`);
|
|
25
|
+
stderr(`coverage-check: ${String(err)}`);
|
|
104
26
|
return 2;
|
|
105
27
|
}
|
|
106
28
|
return runCheck(args);
|
|
@@ -115,25 +37,37 @@ export async function runCheck(args: CheckArgs): Promise<number> {
|
|
|
115
37
|
return 2;
|
|
116
38
|
}
|
|
117
39
|
|
|
40
|
+
const branch = args.branch ?? "main";
|
|
118
41
|
const stripPrefixes = buildStripPrefixes(args.stripPrefixes);
|
|
119
42
|
const reports: LcovData[] = [];
|
|
43
|
+
const suiteSources: SuiteSource[] = [];
|
|
120
44
|
|
|
121
|
-
// Merge in suites from the store (skip the current suite — fresh artifacts take precedence)
|
|
122
45
|
if (args.store !== null) {
|
|
123
46
|
const suites = await args.store.list();
|
|
124
47
|
for (const suite of suites) {
|
|
125
48
|
if (suite === args.suite) continue;
|
|
126
|
-
const buf = await args.store.get(suite);
|
|
49
|
+
const buf = await args.store.get(suite, { branch });
|
|
127
50
|
if (buf !== null) {
|
|
128
|
-
|
|
51
|
+
const lcov = parseLcov(buf.toString("utf8"), stripPrefixes);
|
|
52
|
+
reports.push(lcov);
|
|
53
|
+
suiteSources.push({ suite, source: "store", lcov });
|
|
129
54
|
}
|
|
130
55
|
}
|
|
131
56
|
}
|
|
132
57
|
|
|
133
|
-
// Add current run's lcov files
|
|
134
58
|
const lcovFiles = collectLcovFiles(args.artifacts);
|
|
59
|
+
const freshLcovs: LcovData[] = [];
|
|
135
60
|
for (const f of lcovFiles) {
|
|
136
|
-
|
|
61
|
+
const lcov = parseLcov(readFileSync(f, "utf8"), stripPrefixes);
|
|
62
|
+
reports.push(lcov);
|
|
63
|
+
freshLcovs.push(lcov);
|
|
64
|
+
}
|
|
65
|
+
if (freshLcovs.length > 0) {
|
|
66
|
+
suiteSources.push({
|
|
67
|
+
suite: args.suite ?? "(current)",
|
|
68
|
+
source: "fresh",
|
|
69
|
+
lcov: mergeLcov(freshLcovs),
|
|
70
|
+
});
|
|
137
71
|
}
|
|
138
72
|
|
|
139
73
|
if (reports.length === 0) {
|
|
@@ -167,7 +101,7 @@ export async function runCheck(args: CheckArgs): Promise<number> {
|
|
|
167
101
|
if (!passed) {
|
|
168
102
|
stdout("\ncoverage-check: FAILED\n");
|
|
169
103
|
for (const bucket of buckets.filter((b) => !b.passed)) {
|
|
170
|
-
/* c8 ignore next --
|
|
104
|
+
/* c8 ignore next -- bucket.coverable is always > 0 by patch-coverage.mts L36 guard */
|
|
171
105
|
const pct =
|
|
172
106
|
bucket.coverable > 0 ? `${((bucket.hit / bucket.coverable) * 100).toFixed(1)}%` : "—";
|
|
173
107
|
stdout(
|
|
@@ -180,15 +114,28 @@ export async function runCheck(args: CheckArgs): Promise<number> {
|
|
|
180
114
|
} else {
|
|
181
115
|
stdout("\ncoverage-check: PASSED\n");
|
|
182
116
|
for (const bucket of buckets) {
|
|
183
|
-
/* c8 ignore next --
|
|
117
|
+
/* c8 ignore next -- bucket.coverable is always > 0 by patch-coverage.mts L36 guard */
|
|
184
118
|
const pct =
|
|
185
119
|
bucket.coverable > 0 ? `${((bucket.hit / bucket.coverable) * 100).toFixed(1)}%` : "—";
|
|
186
120
|
stdout(` ${bucket.rule}: ${pct} ✓`);
|
|
187
121
|
}
|
|
188
122
|
}
|
|
189
123
|
|
|
124
|
+
const summaryFile =
|
|
125
|
+
args.summaryFile !== undefined
|
|
126
|
+
? args.summaryFile
|
|
127
|
+
: (process.env["GITHUB_STEP_SUMMARY"] ?? null);
|
|
128
|
+
if (summaryFile) {
|
|
129
|
+
try {
|
|
130
|
+
writeSummary(summaryFile, suiteSources, result, runUrl, branch);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
stderr(`coverage-check: failed to write step summary: ${err}`);
|
|
133
|
+
return 2;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
190
137
|
if (args.pr !== null && args.repo) {
|
|
191
|
-
const body = passed ?
|
|
138
|
+
const body = passed ? "" : renderFailureComment(result, runUrl);
|
|
192
139
|
try {
|
|
193
140
|
await upsertComment(body, args.repo, args.pr, passed, args.gh);
|
|
194
141
|
} catch (err) {
|