@scalequality/check 0.3.1 → 0.4.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 +29 -0
- package/bin/check.mjs +90 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,3 +45,32 @@ scalequality:
|
|
|
45
45
|
|
|
46
46
|
Any other CI: run the same `npx @scalequality/check` command, with `SQ_TOKEN`
|
|
47
47
|
exposed as a secret/variable.
|
|
48
|
+
|
|
49
|
+
## Measured coverage
|
|
50
|
+
|
|
51
|
+
Coverage is only real once the tests have run. If this job produces a coverage
|
|
52
|
+
report, the check finds it and ScaleQuality shows **measured line coverage**
|
|
53
|
+
instead of a directional estimate. Nothing to configure: run your tests with
|
|
54
|
+
coverage before this step.
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
- run: mvn verify # writes target/site/jacoco/jacoco.xml
|
|
58
|
+
- run: npx @scalequality/check --token "$SQ_TOKEN" --project <projectId>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Recognised out of the box: JaCoCo (`jacoco.xml`), LCOV (`lcov.info`), Cobertura
|
|
62
|
+
(`coverage.xml`, `coverage.cobertura.xml`), Istanbul (`coverage-summary.json`),
|
|
63
|
+
Clover (`clover.xml`) and Go (`coverage.out`). Multi-module builds are summed.
|
|
64
|
+
|
|
65
|
+
The reports are copied into a temporary `.sqreports/` folder so they survive the
|
|
66
|
+
archive's build-output exclusions, and that folder is removed before the command
|
|
67
|
+
exits — including when the job is cancelled.
|
|
68
|
+
|
|
69
|
+
Report somewhere unusual? Point at it in `scalequality.properties` at the repo
|
|
70
|
+
root:
|
|
71
|
+
|
|
72
|
+
```properties
|
|
73
|
+
sq.coverage.reportPaths=ops/reports/jacoco-aggregate.xml
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Full guide: https://scalequality.io/docs/continuous-measurement
|
package/bin/check.mjs
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
// build. Pure Node 18+ (global fetch/FormData/Blob), no npm dependencies.
|
|
6
6
|
|
|
7
7
|
import { tmpdir } from "node:os"
|
|
8
|
-
import { join } from "node:path"
|
|
8
|
+
import { join, dirname, relative, sep } from "node:path"
|
|
9
9
|
import { spawnSync } from "node:child_process"
|
|
10
|
-
import { readFileSync, existsSync, statSync, rmSync } from "node:fs"
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync, statSync, rmSync, readdirSync, mkdirSync, copyFileSync } from "node:fs"
|
|
11
11
|
|
|
12
12
|
const args = process.argv.slice(2)
|
|
13
13
|
const flag = (name) => {
|
|
@@ -123,8 +123,92 @@ function slimReport(stdout, ref) {
|
|
|
123
123
|
return json
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
// Coverage reports live in exactly the directories the archive excludes: JaCoCo
|
|
127
|
+
// writes to target/ and build/, Istanbul to coverage/. So the one moment where a
|
|
128
|
+
// REAL coverage number is sitting on disk — right after this job ran the tests —
|
|
129
|
+
// was also the one moment we threw it away, and every Maven project came back
|
|
130
|
+
// with no measured coverage even with a green suite behind it.
|
|
131
|
+
//
|
|
132
|
+
// The reports are copied into .sqreports/ under FLAT, indexed names, with an
|
|
133
|
+
// index.json holding each one's original path. Mirroring the original directory
|
|
134
|
+
// tree looked tidier and did not work: bsdtar does not anchor `--exclude=./target`
|
|
135
|
+
// to the root, so a staged copy at `.scalequality/coverage/target/...` was dropped
|
|
136
|
+
// on macOS while surviving under GNU tar. Measured, not assumed — and a layout
|
|
137
|
+
// whose behaviour depends on which tar the runner ships is not a layout.
|
|
138
|
+
// Every staged name here is `<n>-<basename>`, which no exclude pattern can match.
|
|
139
|
+
const COVERAGE_NAMES = [
|
|
140
|
+
{ re: /^jacoco(TestReport)?\.xml$/i, format: "jacoco" },
|
|
141
|
+
{ re: /^lcov\.info$/i, format: "lcov" },
|
|
142
|
+
{ re: /^coverage-summary\.json$/i, format: "istanbul" },
|
|
143
|
+
{ re: /^coverage\.cobertura\.xml$/i, format: "cobertura" },
|
|
144
|
+
{ re: /^cobertura(-coverage)?\.xml$/i, format: "cobertura" },
|
|
145
|
+
{ re: /^coverage\.xml$/i, format: "cobertura" },
|
|
146
|
+
{ re: /^clover\.xml$/i, format: "clover" },
|
|
147
|
+
{ re: /^(coverage|cover)\.out$/i, format: "go" },
|
|
148
|
+
]
|
|
149
|
+
const COVERAGE_SKIP = new Set(["node_modules", ".git", ".venv", "venv", "vendor", "__pycache__", ".sqreports"])
|
|
150
|
+
const COVERAGE_STAGE = ".sqreports"
|
|
151
|
+
const MAX_REPORT_BYTES = 25 * 1024 * 1024
|
|
152
|
+
const MAX_REPORTS = 40
|
|
153
|
+
|
|
154
|
+
function stageCoverageReports() {
|
|
155
|
+
const root = process.cwd()
|
|
156
|
+
const stage = join(root, COVERAGE_STAGE)
|
|
157
|
+
rmSync(stage, { recursive: true, force: true })
|
|
158
|
+
// The staged copies must not outlive this process under ANY exit path. A
|
|
159
|
+
// cancelled job or a network failure that left them behind would show up as
|
|
160
|
+
// untracked files in the customer's checkout, and a pipeline that runs
|
|
161
|
+
// `git diff --exit-code` after the gate would fail because of us.
|
|
162
|
+
const clean = () => { try { rmSync(stage, { recursive: true, force: true }) } catch { /* best effort */ } }
|
|
163
|
+
process.on("exit", clean)
|
|
164
|
+
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
165
|
+
process.on(sig, () => { clean(); process.exit(130) })
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const index = []
|
|
169
|
+
let bytes = 0
|
|
170
|
+
const walk = (dir, depth) => {
|
|
171
|
+
if (depth > 7 || index.length >= MAX_REPORTS || bytes >= MAX_REPORT_BYTES) return
|
|
172
|
+
let entries
|
|
173
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
174
|
+
for (const e of entries) {
|
|
175
|
+
if (index.length >= MAX_REPORTS || bytes >= MAX_REPORT_BYTES) return
|
|
176
|
+
const abs = join(dir, e.name)
|
|
177
|
+
if (e.isDirectory()) {
|
|
178
|
+
if (!COVERAGE_SKIP.has(e.name)) walk(abs, depth + 1)
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
const hit = COVERAGE_NAMES.find((c) => c.re.test(e.name))
|
|
182
|
+
if (!hit) continue
|
|
183
|
+
let size = 0
|
|
184
|
+
try { size = statSync(abs).size } catch { continue }
|
|
185
|
+
if (size === 0 || bytes + size > MAX_REPORT_BYTES) continue
|
|
186
|
+
const staged = `${index.length}-${e.name}`
|
|
187
|
+
try {
|
|
188
|
+
mkdirSync(stage, { recursive: true })
|
|
189
|
+
copyFileSync(abs, join(stage, staged))
|
|
190
|
+
} catch { continue }
|
|
191
|
+
bytes += size
|
|
192
|
+
index.push({ file: staged, path: relative(root, abs).split(sep).join("/"), format: hit.format })
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
walk(root, 0)
|
|
196
|
+
|
|
197
|
+
if (index.length) {
|
|
198
|
+
try {
|
|
199
|
+
writeFileSync(join(stage, "index.json"), JSON.stringify({ version: 1, reports: index }))
|
|
200
|
+
} catch { /* the archive just goes without them */ }
|
|
201
|
+
const shown = index.slice(0, 3).map((r) => r.path).join(", ")
|
|
202
|
+
log(`ScaleQuality: found ${index.length} coverage report(s) — ${shown}${index.length > 3 ? " …" : ""}`)
|
|
203
|
+
} else {
|
|
204
|
+
log("ScaleQuality: no coverage report found in this job. Run your tests with coverage before this step to get a measured %.")
|
|
205
|
+
}
|
|
206
|
+
return { stage, count: index.length }
|
|
207
|
+
}
|
|
208
|
+
|
|
126
209
|
async function main() {
|
|
127
210
|
const auth = { Authorization: `Bearer ${token}` }
|
|
211
|
+
const coverage = stageCoverageReports()
|
|
128
212
|
|
|
129
213
|
// Upload THIS checkout (the runner already has the code) instead of asking the server
|
|
130
214
|
// to clone the repo from outside — that needed a stored provider credential and failed
|
|
@@ -142,10 +226,14 @@ async function main() {
|
|
|
142
226
|
"-C", process.cwd(), ".",
|
|
143
227
|
], { encoding: "utf8" })
|
|
144
228
|
if (!(existsSync(archivePath) && statSync(archivePath).size > 0)) {
|
|
229
|
+
rmSync(coverage.stage, { recursive: true, force: true })
|
|
145
230
|
fail(`could not archive the checkout (tar unavailable or failed). ${tr.error?.message || (tr.stderr || "").trim()}`)
|
|
146
231
|
}
|
|
147
232
|
const buf = readFileSync(archivePath)
|
|
148
233
|
rmSync(archivePath, { force: true })
|
|
234
|
+
// The staged copies exist only to ride along in the archive. Leaving them
|
|
235
|
+
// behind would dirty the working tree of the job that ran us.
|
|
236
|
+
rmSync(coverage.stage, { recursive: true, force: true })
|
|
149
237
|
const sizeMB = buf.length / (1024 * 1024)
|
|
150
238
|
if (sizeMB > 60) fail(`checkout archive too large (${sizeMB.toFixed(0)}MB > 60MB limit). Reduce clone depth or exclude build output.`)
|
|
151
239
|
|
package/package.json
CHANGED