@scalequality/check 0.4.0 → 0.5.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 +97 -3
- package/bin/archive.mjs +115 -0
- package/bin/check.mjs +41 -34
- package/bin/composer-vendor.mjs +198 -0
- package/bin/durability/GitDurabilityEngine.mjs +225 -0
- package/bin/durability/GitReader.mjs +121 -0
- package/bin/durability/OriginClassifier.mjs +48 -0
- package/bin/durability/PROVENANCE.md +42 -0
- package/bin/durability/provenance.json +53 -0
- package/bin/durability/runtime.mjs +120 -0
- package/bin/durability/types.mjs +5 -0
- package/bin/durability/validate.mjs +31 -0
- package/bin/durability/worker.mjs +44 -0
- package/bin/durability.mjs +65 -0
- package/package.json +4 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { realpath } from 'node:fs/promises'
|
|
2
|
+
import { analyzeDurability } from './GitDurabilityEngine.mjs'
|
|
3
|
+
import { ENGINE_COMMIT, validateReport } from './validate.mjs'
|
|
4
|
+
import { runGit, setDeadline, remainingTime, stopGitChildren, GitMeasurementError } from './runtime.mjs'
|
|
5
|
+
|
|
6
|
+
process.on('SIGTERM', () => { stopGitChildren(); process.exit(2) })
|
|
7
|
+
let completed = false
|
|
8
|
+
process.once('disconnect', () => {
|
|
9
|
+
if (!completed) { stopGitChildren(); process.exit(2) }
|
|
10
|
+
})
|
|
11
|
+
const send = (message) => { if (process.connected) process.send(message) }
|
|
12
|
+
process.once('message', async ({ root, deadline }) => {
|
|
13
|
+
try {
|
|
14
|
+
setDeadline(deadline)
|
|
15
|
+
if ((await runGit(root, ['rev-parse', '--is-inside-work-tree'])).trim() !== 'true') throw new GitMeasurementError('checkout has no working Git history')
|
|
16
|
+
const top = (await runGit(root, ['rev-parse', '--show-toplevel'])).trim()
|
|
17
|
+
if (await realpath(top) !== await realpath(root)) throw new GitMeasurementError('run the check from the repository root to measure Git history')
|
|
18
|
+
if ((await runGit(root, ['rev-parse', '--is-shallow-repository'])).trim() !== 'false') throw new GitMeasurementError('shallow Git history cannot provide the full 90-day measurement')
|
|
19
|
+
const partial = await runGit(root, ['config', '--get-regexp', '^(extensions\.partialclone|remote\..*\.promisor)$'], { allowMissingConfig: true })
|
|
20
|
+
if (partial.split('\n').some(line => /^extensions\.partialclone\s+\S/i.test(line) || /^remote\..*\.promisor\s+(true|yes|on|1)\s*$/i.test(line))) throw new GitMeasurementError('partial Git history cannot be measured without fetching objects')
|
|
21
|
+
const headSha = (await runGit(root, ['rev-parse', '--verify', 'HEAD'])).trim()
|
|
22
|
+
if (!/^[0-9a-f]{40}$/.test(headSha)) throw new GitMeasurementError('a valid SHA-1 Git HEAD is required')
|
|
23
|
+
const objects = await runGit(root, ['rev-list', '--objects', '--no-object-names', '--missing=print', 'HEAD'])
|
|
24
|
+
if (objects.split('\n').some(line => /^\?[0-9a-f]+$/.test(line))) throw new GitMeasurementError('Git history is missing local objects')
|
|
25
|
+
const measuredAt = new Date().toISOString()
|
|
26
|
+
const result = await analyzeDurability(root, {
|
|
27
|
+
windowDays: 90, settleDays: 14,
|
|
28
|
+
onLog: message => { remainingTime(); send({ type: 'log', message }) },
|
|
29
|
+
})
|
|
30
|
+
remainingTime()
|
|
31
|
+
if ((await runGit(root, ['rev-parse', '--verify', 'HEAD'])).trim() !== headSha) throw new GitMeasurementError('Git HEAD changed during measurement')
|
|
32
|
+
const report = { version: 1, source: 'ci', engineCommit: ENGINE_COMMIT, windowDays: 90, settleDays: 14, headSha, measuredAt, result }
|
|
33
|
+
if (!validateReport(report)) throw new GitMeasurementError('native measurement did not pass report validation')
|
|
34
|
+
send({ type: 'result', report })
|
|
35
|
+
completed = true
|
|
36
|
+
process.disconnect()
|
|
37
|
+
} catch (error) {
|
|
38
|
+
send({ type: 'failure', reason: error instanceof GitMeasurementError ? error.reason : 'local Git measurement failed' })
|
|
39
|
+
stopGitChildren()
|
|
40
|
+
process.exitCode = 2
|
|
41
|
+
completed = true
|
|
42
|
+
if (process.connected) process.disconnect()
|
|
43
|
+
}
|
|
44
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { fork } from 'node:child_process'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import { validateReport } from './durability/validate.mjs'
|
|
5
|
+
import { localGitEnvironment } from './durability/runtime.mjs'
|
|
6
|
+
export { ENGINE_COMMIT } from './durability/validate.mjs'
|
|
7
|
+
export const MAX_MEASUREMENT_MS = 120_000
|
|
8
|
+
|
|
9
|
+
/** Measure committed local history; never clone, fetch, checkout or upload it. */
|
|
10
|
+
export async function measureDurability({ root, log = () => {} }) {
|
|
11
|
+
const notify = message => { try { log(message) } catch {} }
|
|
12
|
+
return new Promise(resolveResult => {
|
|
13
|
+
let worker
|
|
14
|
+
let result = null
|
|
15
|
+
let reason = 'local Git measurement failed'
|
|
16
|
+
let done = false
|
|
17
|
+
const started = Date.now()
|
|
18
|
+
const killOwnedGroup = () => {
|
|
19
|
+
if (!worker?.pid) return
|
|
20
|
+
try {
|
|
21
|
+
if (process.platform !== 'win32') process.kill(-worker.pid, 'SIGKILL')
|
|
22
|
+
else worker.kill('SIGTERM')
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
const finish = value => {
|
|
26
|
+
if (done) return
|
|
27
|
+
done = true
|
|
28
|
+
clearTimeout(timer)
|
|
29
|
+
process.removeListener('SIGINT', onInterrupt)
|
|
30
|
+
process.removeListener('SIGTERM', onTerminate)
|
|
31
|
+
process.removeListener('exit', killOwnedGroup)
|
|
32
|
+
if (!value) { killOwnedGroup(); notify(`ScaleQuality: Git durability not measured — ${reason}. No Git history was uploaded.`) }
|
|
33
|
+
resolveResult(value)
|
|
34
|
+
}
|
|
35
|
+
const cancel = signal => {
|
|
36
|
+
const hasOtherHandler = process.listenerCount(signal) > 1
|
|
37
|
+
reason = 'measurement was cancelled'
|
|
38
|
+
finish(null)
|
|
39
|
+
// Preserve the caller's signal handling, including Node's default exit.
|
|
40
|
+
if (!hasOtherHandler) process.kill(process.pid, signal)
|
|
41
|
+
}
|
|
42
|
+
const onInterrupt = () => cancel('SIGINT')
|
|
43
|
+
const onTerminate = () => cancel('SIGTERM')
|
|
44
|
+
const timer = setTimeout(() => { reason = 'the 120-second measurement budget was exceeded'; finish(null) }, MAX_MEASUREMENT_MS)
|
|
45
|
+
process.on('SIGINT', onInterrupt)
|
|
46
|
+
process.on('SIGTERM', onTerminate)
|
|
47
|
+
process.on('exit', killOwnedGroup)
|
|
48
|
+
try {
|
|
49
|
+
worker = fork(fileURLToPath(new URL('./durability/worker.mjs', import.meta.url)), [], {
|
|
50
|
+
stdio: ['ignore', 'ignore', 'ignore', 'ipc'], env: localGitEnvironment(),
|
|
51
|
+
execArgv: ['--max-old-space-size=512'], detached: process.platform !== 'win32',
|
|
52
|
+
})
|
|
53
|
+
worker.on('message', message => {
|
|
54
|
+
if (message?.type === 'log' && typeof message.message === 'string' && /^(commits in window: \d+|blame capped: \d+\/\d+ files \(perf\)|blaming \d+ files\.\.\.)$/.test(message.message)) notify(`ScaleQuality: durability — ${message.message}`)
|
|
55
|
+
else if (message?.type === 'result' && validateReport(message.report)) result = message.report
|
|
56
|
+
else if (message?.type === 'failure') reason = typeof message.reason === 'string' ? message.reason : reason
|
|
57
|
+
})
|
|
58
|
+
worker.on('error', () => finish(null))
|
|
59
|
+
worker.on('exit', code => finish(code === 0 && result && validateReport(result) ? result : null))
|
|
60
|
+
worker.send({ root: resolve(root), deadline: started + MAX_MEASUREMENT_MS - 1000 })
|
|
61
|
+
} catch { finish(null) }
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const ENGINE_SOURCE_DIGEST = "bc7a01ce93df3aad2f80da281934868ba7327a1a04fad840f35ecc8b422c74cf"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalequality/check",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "ScaleQuality continuous code-maturity check for CI. Re-measures your repo on every build and fails the pipeline below your maturity line.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,5 +26,8 @@
|
|
|
26
26
|
"homepage": "https://scalequality.io",
|
|
27
27
|
"publishConfig": {
|
|
28
28
|
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "node --test test/*.test.mjs"
|
|
29
32
|
}
|
|
30
33
|
}
|