coverage-check 0.7.0 → 0.8.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/README.md +119 -3
- package/dist/src/cli.mjs +25 -0
- package/dist/src/commands/check-args.d.mts +6 -0
- package/dist/src/commands/check-args.mjs +20 -2
- package/dist/src/commands/check-output.d.mts +5 -0
- package/dist/src/commands/check-output.mjs +15 -10
- package/dist/src/commands/check-render.d.mts +1 -0
- package/dist/src/commands/check-render.mjs +27 -0
- package/dist/src/commands/check-result.d.mts +17 -0
- package/dist/src/commands/check-result.mjs +12 -0
- package/dist/src/commands/check.d.mts +21 -0
- package/dist/src/commands/check.mjs +150 -49
- package/dist/src/commands/prepare-artifacts.d.mts +18 -0
- package/dist/src/commands/prepare-artifacts.mjs +105 -0
- package/dist/src/coverage-check.d.mts +6 -2
- package/dist/src/coverage-check.mjs +4 -1
- package/dist/src/github-comment.mjs +6 -4
- package/dist/src/lcov-records.mjs +13 -4
- package/dist/src/rules.d.mts +4 -0
- package/dist/src/rules.mjs +10 -0
- package/dist/src/s3-diagnostics.d.mts +9 -0
- package/dist/src/s3-diagnostics.mjs +60 -0
- package/dist/src/s3-suite-store-reads.d.mts +17 -0
- package/dist/src/s3-suite-store-reads.mjs +57 -0
- package/dist/src/s3-suite-store.d.mts +7 -10
- package/dist/src/s3-suite-store.mjs +68 -75
- package/dist/src/suite-store.d.mts +1 -0
- package/dist/src/suite-store.mjs +22 -2
- package/package.json +4 -3
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { GetObjectCommand } from "@aws-sdk/client-s3";
|
|
2
|
+
import { encodeBranchName, isNewerTimestamp } from "./suite-store.mjs";
|
|
3
|
+
import { bodyToBuffer, isNotFound } from "./s3-utils.mjs";
|
|
4
|
+
export async function getLegacy(ctx, suite) {
|
|
5
|
+
const key = ctx.key(suite, "lcov.info");
|
|
6
|
+
try {
|
|
7
|
+
const resp = (await ctx.sendS3("get legacy coverage payload", key, new GetObjectCommand({
|
|
8
|
+
Bucket: ctx.bucket,
|
|
9
|
+
Key: key,
|
|
10
|
+
})));
|
|
11
|
+
return bodyToBuffer(resp.Body);
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
if (isNotFound(err))
|
|
15
|
+
return null;
|
|
16
|
+
throw err;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function shouldWritePointer(ctx, suite, encodedBranch, incomingTimestamp) {
|
|
20
|
+
const key = ctx.key(suite, "branch", encodedBranch, "latest.json");
|
|
21
|
+
try {
|
|
22
|
+
const resp = (await ctx.sendS3("get branch pointer for compare", key, new GetObjectCommand({
|
|
23
|
+
Bucket: ctx.bucket,
|
|
24
|
+
Key: key,
|
|
25
|
+
})));
|
|
26
|
+
if (resp.Body === undefined)
|
|
27
|
+
return true;
|
|
28
|
+
const body = await bodyToBuffer(resp.Body);
|
|
29
|
+
const current = JSON.parse(body.toString("utf8"));
|
|
30
|
+
return !isNewerTimestamp(current.timestamp, incomingTimestamp);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (isNotFound(err))
|
|
34
|
+
return true;
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function readPointer(ctx, suite, branch) {
|
|
39
|
+
const keys = [
|
|
40
|
+
ctx.key(suite, "branch", encodeBranchName(branch), "latest.json"),
|
|
41
|
+
ctx.key(suite, "branch", branch, "latest.json"),
|
|
42
|
+
];
|
|
43
|
+
let lastNotFound;
|
|
44
|
+
for (const key of keys) {
|
|
45
|
+
try {
|
|
46
|
+
const resp = (await ctx.sendS3("get branch pointer", key, new GetObjectCommand({ Bucket: ctx.bucket, Key: key })));
|
|
47
|
+
const body = await bodyToBuffer(resp.Body);
|
|
48
|
+
return JSON.parse(body.toString("utf8"));
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (!isNotFound(err))
|
|
52
|
+
throw err;
|
|
53
|
+
lastNotFound = err;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
throw lastNotFound;
|
|
57
|
+
}
|
|
@@ -1,28 +1,25 @@
|
|
|
1
|
+
import type { ClientLike, S3OperationDetails } from "./s3-diagnostics.mts";
|
|
1
2
|
import type { SuitePutMeta, SuiteStore } from "./suite-store.mts";
|
|
2
|
-
type ClientLike = {
|
|
3
|
-
send(cmd: object): Promise<unknown>;
|
|
4
|
-
};
|
|
5
3
|
export type S3SuiteStoreOptions = {
|
|
6
4
|
bucket: string;
|
|
7
5
|
prefix?: string;
|
|
8
6
|
region?: string;
|
|
9
|
-
/** Inject a custom S3 client (e.g. for testing). */
|
|
10
7
|
client?: ClientLike;
|
|
11
8
|
};
|
|
12
9
|
export declare class S3SuiteStore implements SuiteStore {
|
|
13
|
-
|
|
10
|
+
readonly bucket: string;
|
|
14
11
|
private readonly prefix;
|
|
15
12
|
private readonly client;
|
|
16
13
|
constructor({ bucket, prefix, region, client }: S3SuiteStoreOptions);
|
|
17
|
-
|
|
14
|
+
key(...parts: string[]): string;
|
|
18
15
|
list(): Promise<string[]>;
|
|
19
16
|
get(suite: string, opts?: {
|
|
20
17
|
sha?: string;
|
|
21
18
|
branch?: string;
|
|
22
19
|
}): Promise<Buffer | null>;
|
|
23
20
|
put(suite: string, lcov: Buffer, meta?: SuitePutMeta): Promise<void>;
|
|
24
|
-
|
|
25
|
-
private
|
|
26
|
-
private
|
|
21
|
+
sendS3(operation: string, key: string, command: object, details?: S3OperationDetails): Promise<unknown>;
|
|
22
|
+
private getVersionedPayload;
|
|
23
|
+
private putLegacyPayload;
|
|
24
|
+
private putPointer;
|
|
27
25
|
}
|
|
28
|
-
export {};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { GetObjectCommand, ListObjectsV2Command, PutObjectCommand
|
|
2
|
-
import {
|
|
1
|
+
import { GetObjectCommand, ListObjectsV2Command, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
2
|
+
import { gunzipSync, gzipSync } from "node:zlib";
|
|
3
|
+
import { assertSafePathComponent, assertValidTimestamp, encodeBranchName } from "./suite-store.mjs";
|
|
4
|
+
import { createS3Client, sendS3 } from "./s3-diagnostics.mjs";
|
|
5
|
+
import { getLegacy, readPointer, shouldWritePointer } from "./s3-suite-store-reads.mjs";
|
|
3
6
|
import { bodyToBuffer, isNotFound } from "./s3-utils.mjs";
|
|
4
7
|
export class S3SuiteStore {
|
|
5
8
|
bucket;
|
|
@@ -8,7 +11,7 @@ export class S3SuiteStore {
|
|
|
8
11
|
constructor({ bucket, prefix, region, client }) {
|
|
9
12
|
this.bucket = bucket;
|
|
10
13
|
this.prefix = prefix ? prefix.replace(/\/+$/, "") : "";
|
|
11
|
-
this.client = client ??
|
|
14
|
+
this.client = client ?? createS3Client(region);
|
|
12
15
|
}
|
|
13
16
|
key(...parts) {
|
|
14
17
|
return this.prefix ? [this.prefix, ...parts].join("/") : parts.join("/");
|
|
@@ -18,7 +21,7 @@ export class S3SuiteStore {
|
|
|
18
21
|
const suites = [];
|
|
19
22
|
let continuationToken;
|
|
20
23
|
do {
|
|
21
|
-
const resp = (await this.
|
|
24
|
+
const resp = (await this.sendS3("list suites", this.key(), new ListObjectsV2Command({
|
|
22
25
|
Bucket: this.bucket,
|
|
23
26
|
Prefix: pfx,
|
|
24
27
|
Delimiter: "/",
|
|
@@ -36,112 +39,102 @@ export class S3SuiteStore {
|
|
|
36
39
|
if (opts?.sha !== undefined)
|
|
37
40
|
assertSafePathComponent(opts.sha, "sha");
|
|
38
41
|
let sha = opts?.sha;
|
|
42
|
+
let pointer = null;
|
|
39
43
|
if (!sha) {
|
|
40
44
|
const branch = opts?.branch ?? "main";
|
|
41
45
|
try {
|
|
42
|
-
|
|
46
|
+
pointer = await readPointer(this, suite, branch);
|
|
43
47
|
assertSafePathComponent(pointer.sha, "sha");
|
|
44
48
|
sha = pointer.sha;
|
|
45
49
|
}
|
|
46
50
|
catch (err) {
|
|
47
51
|
if (isNotFound(err))
|
|
48
|
-
return
|
|
52
|
+
return getLegacy(this, suite);
|
|
49
53
|
throw err;
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
|
-
|
|
53
|
-
const resp = (await this.client.send(new GetObjectCommand({
|
|
54
|
-
Bucket: this.bucket,
|
|
55
|
-
Key: this.key(suite, "sha", sha, "lcov.info"),
|
|
56
|
-
})));
|
|
57
|
-
return bodyToBuffer(resp.Body);
|
|
58
|
-
}
|
|
59
|
-
catch (err) {
|
|
60
|
-
if (isNotFound(err))
|
|
61
|
-
return null;
|
|
62
|
-
throw err;
|
|
63
|
-
}
|
|
56
|
+
return this.getVersionedPayload(suite, sha, pointer, opts?.sha !== undefined);
|
|
64
57
|
}
|
|
65
58
|
async put(suite, lcov, meta) {
|
|
66
59
|
assertSafePathComponent(suite, "suite");
|
|
67
60
|
if (meta === undefined) {
|
|
68
|
-
await this.
|
|
69
|
-
Bucket: this.bucket,
|
|
70
|
-
Key: this.key(suite, "lcov.info"),
|
|
71
|
-
Body: lcov,
|
|
72
|
-
ContentType: "text/plain",
|
|
73
|
-
}));
|
|
61
|
+
await this.putLegacyPayload(suite, lcov);
|
|
74
62
|
return;
|
|
75
63
|
}
|
|
76
64
|
const { sha, branch } = meta;
|
|
77
65
|
assertSafePathComponent(sha, "sha");
|
|
66
|
+
const encodedBranch = encodeBranchName(branch);
|
|
78
67
|
const ts = meta.timestamp ?? new Date().toISOString();
|
|
79
68
|
assertValidTimestamp(ts);
|
|
80
|
-
|
|
69
|
+
const payload = gzipSync(lcov);
|
|
70
|
+
const payloadKey = this.key(suite, "sha", sha, "lcov.info.gz");
|
|
71
|
+
await this.sendS3("put coverage payload", payloadKey, new PutObjectCommand({
|
|
81
72
|
Bucket: this.bucket,
|
|
82
|
-
Key:
|
|
83
|
-
Body:
|
|
73
|
+
Key: payloadKey,
|
|
74
|
+
Body: payload,
|
|
75
|
+
ContentEncoding: "gzip",
|
|
84
76
|
ContentType: "text/plain",
|
|
85
|
-
}));
|
|
86
|
-
if (!(await
|
|
77
|
+
}), { rawBytes: lcov.byteLength, storedBytes: payload.byteLength });
|
|
78
|
+
if (!(await shouldWritePointer(this, suite, encodedBranch, ts)))
|
|
87
79
|
return;
|
|
88
|
-
await this.
|
|
89
|
-
Bucket: this.bucket,
|
|
90
|
-
Key: this.key(suite, "branch", encodeBranchName(branch), "latest.json"),
|
|
91
|
-
Body: Buffer.from(JSON.stringify({ sha, timestamp: ts }), "utf8"),
|
|
92
|
-
ContentType: "application/json",
|
|
93
|
-
}));
|
|
80
|
+
await this.putPointer(suite, branch, sha, ts, payloadKey, lcov.byteLength, payload.byteLength);
|
|
94
81
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const resp = (await this.client.send(new GetObjectCommand({
|
|
98
|
-
Bucket: this.bucket,
|
|
99
|
-
Key: this.key(suite, "lcov.info"),
|
|
100
|
-
})));
|
|
101
|
-
return bodyToBuffer(resp.Body);
|
|
102
|
-
}
|
|
103
|
-
catch (err) {
|
|
104
|
-
if (isNotFound(err))
|
|
105
|
-
return null;
|
|
106
|
-
throw err;
|
|
107
|
-
}
|
|
82
|
+
sendS3(operation, key, command, details = {}) {
|
|
83
|
+
return sendS3(this.client, this.bucket, operation, key, command, details);
|
|
108
84
|
}
|
|
109
|
-
async
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
Key: this.key(suite, "branch", encodeBranchName(branch), "latest.json"),
|
|
114
|
-
})));
|
|
115
|
-
if (resp.Body === undefined)
|
|
116
|
-
return true;
|
|
117
|
-
const body = await bodyToBuffer(resp.Body);
|
|
118
|
-
const current = JSON.parse(body.toString("utf8"));
|
|
119
|
-
return !isNewerTimestamp(current.timestamp, incomingTimestamp);
|
|
85
|
+
async getVersionedPayload(suite, sha, pointer, explicitSha) {
|
|
86
|
+
let candidates;
|
|
87
|
+
if (pointer?.payloadKey !== undefined) {
|
|
88
|
+
candidates = [{ key: pointer.payloadKey, encoding: pointer.encoding }];
|
|
120
89
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
90
|
+
else if (explicitSha) {
|
|
91
|
+
candidates = [
|
|
92
|
+
{ key: this.key(suite, "sha", sha, "lcov.info.gz"), encoding: "gzip" },
|
|
93
|
+
{ key: this.key(suite, "sha", sha, "lcov.info"), encoding: undefined },
|
|
94
|
+
];
|
|
125
95
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
this.key(suite, "branch", branch, "latest.json"),
|
|
131
|
-
];
|
|
132
|
-
let lastNotFound;
|
|
133
|
-
for (const key of keys) {
|
|
96
|
+
else {
|
|
97
|
+
candidates = [{ key: this.key(suite, "sha", sha, "lcov.info"), encoding: undefined }];
|
|
98
|
+
}
|
|
99
|
+
for (const candidate of candidates) {
|
|
134
100
|
try {
|
|
135
|
-
const resp = (await this.
|
|
101
|
+
const resp = (await this.sendS3("get coverage payload", candidate.key, new GetObjectCommand({
|
|
102
|
+
Bucket: this.bucket,
|
|
103
|
+
Key: candidate.key,
|
|
104
|
+
})));
|
|
136
105
|
const body = await bodyToBuffer(resp.Body);
|
|
137
|
-
return
|
|
106
|
+
return candidate.encoding === "gzip" ? gunzipSync(body) : body;
|
|
138
107
|
}
|
|
139
108
|
catch (err) {
|
|
140
109
|
if (!isNotFound(err))
|
|
141
110
|
throw err;
|
|
142
|
-
lastNotFound = err;
|
|
143
111
|
}
|
|
144
112
|
}
|
|
145
|
-
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
async putLegacyPayload(suite, lcov) {
|
|
116
|
+
const key = this.key(suite, "lcov.info");
|
|
117
|
+
await this.sendS3("put legacy coverage payload", key, new PutObjectCommand({
|
|
118
|
+
Bucket: this.bucket,
|
|
119
|
+
Key: key,
|
|
120
|
+
Body: lcov,
|
|
121
|
+
ContentType: "text/plain",
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
async putPointer(suite, branch, sha, timestamp, payloadKey, rawBytes, storedBytes) {
|
|
125
|
+
const pointerKey = this.key(suite, "branch", encodeBranchName(branch), "latest.json");
|
|
126
|
+
await this.sendS3("put branch pointer", pointerKey, new PutObjectCommand({
|
|
127
|
+
Bucket: this.bucket,
|
|
128
|
+
Key: pointerKey,
|
|
129
|
+
Body: Buffer.from(JSON.stringify({
|
|
130
|
+
sha,
|
|
131
|
+
timestamp,
|
|
132
|
+
payloadKey,
|
|
133
|
+
encoding: "gzip",
|
|
134
|
+
rawBytes,
|
|
135
|
+
storedBytes,
|
|
136
|
+
}), "utf8"),
|
|
137
|
+
ContentType: "application/json",
|
|
138
|
+
}));
|
|
146
139
|
}
|
|
147
140
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SuiteMeta } from "./types.mts";
|
|
2
2
|
export declare function assertSafePathComponent(value: string, label: string): void;
|
|
3
|
+
export declare function assertValidRepo(repo: string): string;
|
|
3
4
|
export type { SuiteMeta };
|
|
4
5
|
export type SuitePutMeta = {
|
|
5
6
|
sha: string;
|
package/dist/src/suite-store.mjs
CHANGED
|
@@ -10,8 +10,27 @@ export function assertSafePathComponent(value, label) {
|
|
|
10
10
|
throw new Error(`invalid ${label}: ${JSON.stringify(value)}`);
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
+
export function assertValidRepo(repo) {
|
|
14
|
+
const trimmed = typeof repo === "string" ? repo.trim() : "";
|
|
15
|
+
if (trimmed.length === 0) {
|
|
16
|
+
throw new Error(`Invalid repository format: ${repo}. Expected owner/repo.`);
|
|
17
|
+
}
|
|
18
|
+
const parts = trimmed.split("/");
|
|
19
|
+
if (parts.length !== 2 ||
|
|
20
|
+
!/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(parts[0]) ||
|
|
21
|
+
!/^[A-Za-z0-9_.-]+$/.test(parts[1]) ||
|
|
22
|
+
parts[1] === "." ||
|
|
23
|
+
parts[1] === "..") {
|
|
24
|
+
throw new Error(`Invalid repository format: ${repo}. Expected owner/repo.`);
|
|
25
|
+
}
|
|
26
|
+
return trimmed;
|
|
27
|
+
}
|
|
13
28
|
export function encodeBranchName(branch) {
|
|
14
|
-
if (typeof branch !== "string" ||
|
|
29
|
+
if (typeof branch !== "string" ||
|
|
30
|
+
branch.length === 0 ||
|
|
31
|
+
branch === "." ||
|
|
32
|
+
branch.includes("..") ||
|
|
33
|
+
branch.includes("\\")) {
|
|
15
34
|
throw new Error(`invalid branch: ${JSON.stringify(branch)}`);
|
|
16
35
|
}
|
|
17
36
|
return Buffer.from(branch, "utf8").toString("base64url");
|
|
@@ -91,10 +110,11 @@ export class FileSystemSuiteStore {
|
|
|
91
110
|
}
|
|
92
111
|
const { sha, branch } = meta;
|
|
93
112
|
assertSafePathComponent(sha, "sha");
|
|
113
|
+
const encodedBranch = encodeBranchName(branch);
|
|
94
114
|
const shaDir = join(this.root, suite, "sha", sha);
|
|
95
115
|
mkdirSync(shaDir, { recursive: true });
|
|
96
116
|
writeFileSync(join(shaDir, "lcov.info"), lcov);
|
|
97
|
-
const branchDir = join(this.root, suite, "branch",
|
|
117
|
+
const branchDir = join(this.root, suite, "branch", encodedBranch);
|
|
98
118
|
mkdirSync(branchDir, { recursive: true });
|
|
99
119
|
const pointerPath = join(branchDir, "latest.json");
|
|
100
120
|
const timestamp = meta.timestamp ?? new Date().toISOString();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coverage-check",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"node": ">=22.0.0"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@aws-sdk/client-s3": "
|
|
34
|
+
"@aws-sdk/client-s3": "3.1075.0",
|
|
35
|
+
"@smithy/node-http-handler": "^4.7.7",
|
|
35
36
|
"js-yaml": "^4.1.1",
|
|
36
37
|
"monocart-coverage-reports": "^2.12.11"
|
|
37
38
|
},
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
"@types/node": "^25.8.0",
|
|
41
42
|
"@vitest/coverage-v8": "^4.1.6",
|
|
42
43
|
"husky": "^9.1.7",
|
|
43
|
-
"oxfmt": "^0.
|
|
44
|
+
"oxfmt": "^0.53.0",
|
|
44
45
|
"oxlint": "^1.65.0",
|
|
45
46
|
"typescript": "^6.0.3",
|
|
46
47
|
"vitest": "^4.1.6"
|