fg-devkit 1.20.0 → 1.21.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 +30 -6
- package/configs/dependency-check-suppressions.xml +17 -0
- package/configs/dependency-policy.vue-workspace.json +1 -1
- package/package.json +3 -3
- package/scaffold/vue/package.json +2 -2
- package/scripts/audit-fix-plan.mjs +436 -0
- package/scripts/audit-fix-plan.test.mjs +287 -0
- package/scripts/audit-override-commit.mjs +380 -81
- package/scripts/fg-devkit.mjs +6 -6
- package/scripts/sync-dependency-check-suppressions-commit.mjs +78 -14
|
@@ -1,16 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Fix
|
|
4
|
-
*
|
|
3
|
+
* Fix `pnpm audit` findings with the smallest possible change, verify, and auto-commit.
|
|
4
|
+
*
|
|
5
|
+
* 1. Audit and collect the vulnerable packages.
|
|
6
|
+
* 2. Refresh them in the lockfile (`pnpm update <names>`): newest version each parent's
|
|
7
|
+
* range already allows. No overrides, nothing to clean up later.
|
|
8
|
+
* 3. Audit again. Remaining findings with a patched version that a parent's range blocks
|
|
9
|
+
* get a tight override (`name@>=installed <patched`: lowest patched version) in
|
|
10
|
+
* pnpm-workspace.yaml. Findings with no patch, a patch in another major, a patch
|
|
11
|
+
* younger than minimumReleaseAge, or on a direct dependency are reported, not overridden.
|
|
12
|
+
* 4. Prune overrides that no longer change the audit (prune-overrides --fix).
|
|
13
|
+
* 5. If findings that are still reported have active <suppress> entries in fg-devkit's
|
|
14
|
+
* shared dependency-check-suppressions.xml, write the repo's copy with only those
|
|
15
|
+
* entries, so the Jenkins OWASP scan accepts exactly the risks this repo has.
|
|
16
|
+
* 6. Install and run the repo's checks (lint-check, lint:ts, test, test:unit, build).
|
|
17
|
+
* 7. Commit the refresh, the override changes and the suppressions file separately.
|
|
5
18
|
*
|
|
6
19
|
* Usage:
|
|
7
|
-
* pnpm exec fg-devkit audit-override-commit [
|
|
20
|
+
* pnpm exec fg-devkit audit-override-commit [--no-verify] [--no-commit]
|
|
8
21
|
*/
|
|
9
22
|
|
|
10
23
|
import fs from 'node:fs'
|
|
11
24
|
import path from 'node:path'
|
|
12
25
|
import { spawnSync } from 'node:child_process'
|
|
13
26
|
import { fileURLToPath } from 'node:url'
|
|
27
|
+
import { parseArgs } from 'node:util'
|
|
28
|
+
import {
|
|
29
|
+
DEFAULT_MINIMUM_RELEASE_AGE_MINUTES,
|
|
30
|
+
activeSuppressedGhsas,
|
|
31
|
+
buildRefreshCommitMessage,
|
|
32
|
+
filterSuppressions,
|
|
33
|
+
diffLockfileVersions,
|
|
34
|
+
isReleaseAgeExcluded,
|
|
35
|
+
normalizeAdvisories,
|
|
36
|
+
parseAuditJson,
|
|
37
|
+
planOverrides,
|
|
38
|
+
upsertWorkspaceOverrides,
|
|
39
|
+
} from './audit-fix-plan.mjs'
|
|
14
40
|
import {
|
|
15
41
|
buildOverrideCommitMessage,
|
|
16
42
|
collectOverrideChanges,
|
|
@@ -18,120 +44,393 @@ import {
|
|
|
18
44
|
formatOverrideValue,
|
|
19
45
|
} from './override-changes.mjs'
|
|
20
46
|
|
|
47
|
+
const TAG = '[audit-override-commit]'
|
|
21
48
|
const cwd = process.cwd()
|
|
22
49
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
|
23
|
-
const
|
|
50
|
+
const TRACKED_FILES = ['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml']
|
|
51
|
+
const VERIFY_SCRIPTS = ['lint-check', 'lint:ts', 'test', 'test:unit', 'build']
|
|
52
|
+
const SUPPRESSIONS_FILE = 'dependency-check-suppressions.xml'
|
|
53
|
+
const SUPPRESSIONS_COMMIT_MESSAGE = 'chore(repo): sync dependency-check suppressions'
|
|
54
|
+
let suppressionsWritten = false
|
|
55
|
+
|
|
56
|
+
const { values: args } = parseArgs({
|
|
57
|
+
options: {
|
|
58
|
+
'no-verify': { type: 'boolean', default: false },
|
|
59
|
+
'no-commit': { type: 'boolean', default: false },
|
|
60
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
61
|
+
},
|
|
62
|
+
strict: true,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
if (args.help) {
|
|
66
|
+
console.log(`Usage: pnpm exec fg-devkit audit-override-commit [--no-verify] [--no-commit]
|
|
67
|
+
|
|
68
|
+
Refreshes vulnerable packages in the lockfile within their existing ranges, adds a
|
|
69
|
+
tight override only where a parent's range blocks the patched version, prunes unused
|
|
70
|
+
overrides, syncs dependency-check-suppressions.xml when a remaining finding is
|
|
71
|
+
suppressed there, runs the repo's checks, and commits each kind of change separately.
|
|
72
|
+
|
|
73
|
+
--no-verify Skip lint/typecheck/test/build (git hooks still run on commit)
|
|
74
|
+
--no-commit Leave the changes in the working tree`)
|
|
75
|
+
process.exit(0)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Process helpers
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
24
81
|
|
|
25
|
-
function
|
|
26
|
-
const result = spawnSync(
|
|
27
|
-
stdio: 'inherit',
|
|
82
|
+
function run(command, commandArgs, { capture = false, env = {}, input } = {}) {
|
|
83
|
+
const result = spawnSync(command, commandArgs, {
|
|
28
84
|
cwd,
|
|
29
|
-
env: process.env,
|
|
85
|
+
env: { ...process.env, ...env },
|
|
86
|
+
encoding: 'utf8',
|
|
87
|
+
stdio: capture ? ['pipe', 'pipe', 'pipe'] : ['inherit', 'inherit', 'inherit'],
|
|
88
|
+
input,
|
|
89
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
30
90
|
})
|
|
31
|
-
if (result.error)
|
|
32
|
-
|
|
33
|
-
|
|
91
|
+
if (result.error) throw result.error
|
|
92
|
+
return result
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function runOrFail(command, commandArgs, options) {
|
|
96
|
+
const result = run(command, commandArgs, options)
|
|
97
|
+
if (result.status !== 0) {
|
|
98
|
+
const detail = options?.capture ? `\n${(result.stderr || result.stdout || '').trim()}` : ''
|
|
99
|
+
throw new Error(`${command} ${commandArgs.join(' ')} failed (exit ${result.status})${detail}`)
|
|
34
100
|
}
|
|
35
|
-
return result
|
|
101
|
+
return result
|
|
36
102
|
}
|
|
37
103
|
|
|
38
|
-
function
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
104
|
+
function readFile(file) {
|
|
105
|
+
const full = path.join(cwd, file)
|
|
106
|
+
return fs.existsSync(full) ? fs.readFileSync(full, 'utf8') : undefined
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function snapshot() {
|
|
110
|
+
return Object.fromEntries(TRACKED_FILES.map((file) => [file, readFile(file)]))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isWorkspaceWithPackages() {
|
|
114
|
+
return /^packages:/m.test(readFile('pnpm-workspace.yaml') ?? '')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// pnpm
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
function audit() {
|
|
122
|
+
const result = run('pnpm', ['audit', '--json'], { capture: true })
|
|
123
|
+
if (result.status !== 0 && result.status !== 1) {
|
|
124
|
+
throw new Error(`pnpm audit failed (exit ${result.status})\n${(result.stderr || result.stdout || '').trim()}`)
|
|
47
125
|
}
|
|
48
|
-
return result.
|
|
126
|
+
return normalizeAdvisories(parseAuditJson(result.stdout))
|
|
49
127
|
}
|
|
50
128
|
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
129
|
+
function readPnpmConfig() {
|
|
130
|
+
const result = run('pnpm', ['config', 'list', '--json'], { capture: true })
|
|
131
|
+
if (result.status !== 0) return {}
|
|
132
|
+
try {
|
|
133
|
+
return parseAuditJson(result.stdout)
|
|
134
|
+
} catch {
|
|
135
|
+
return {}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function publishTimes(name) {
|
|
140
|
+
const result = run('pnpm', ['view', name, 'time', '--json'], { capture: true })
|
|
141
|
+
if (result.status !== 0) return {}
|
|
142
|
+
try {
|
|
143
|
+
return parseAuditJson(result.stdout)
|
|
144
|
+
} catch {
|
|
145
|
+
return {}
|
|
61
146
|
}
|
|
62
|
-
return result.status ?? 1
|
|
63
147
|
}
|
|
64
148
|
|
|
65
|
-
function
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
149
|
+
function directDependencies() {
|
|
150
|
+
const pkg = JSON.parse(readFile('package.json'))
|
|
151
|
+
return new Set(
|
|
152
|
+
['dependencies', 'devDependencies', 'optionalDependencies'].flatMap((section) => Object.keys(pkg[section] ?? {})),
|
|
153
|
+
)
|
|
69
154
|
}
|
|
70
155
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// git
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
function assertCleanGitState() {
|
|
161
|
+
const files = [...TRACKED_FILES, SUPPRESSIONS_FILE]
|
|
162
|
+
const status = runOrFail('git', ['status', '--porcelain', '--', ...files], { capture: true }).stdout.trim()
|
|
163
|
+
const problem = status
|
|
164
|
+
? `Commit or stash your changes to ${files.join(', ')} first:\n${status}`
|
|
165
|
+
: run('git', ['diff', '--cached', '--quiet'], { capture: true }).status !== 0
|
|
166
|
+
? 'There are staged changes. Commit or unstage them first.'
|
|
167
|
+
: undefined
|
|
168
|
+
if (problem) {
|
|
169
|
+
console.error(`${TAG} ${problem}`)
|
|
74
170
|
process.exit(1)
|
|
75
171
|
}
|
|
172
|
+
}
|
|
76
173
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
174
|
+
function headContent(file) {
|
|
175
|
+
const result = run('git', ['show', `HEAD:${file}`], { capture: true })
|
|
176
|
+
return result.status === 0 ? result.stdout : undefined
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Stage given file contents (not necessarily what is on disk) without touching the working tree. */
|
|
180
|
+
function stageContents(contents) {
|
|
181
|
+
let staged = 0
|
|
182
|
+
for (const [file, content] of Object.entries(contents)) {
|
|
183
|
+
if (content === undefined || content === headContent(file)) continue
|
|
184
|
+
const sha = runOrFail('git', ['hash-object', '-w', '--stdin', `--path=${file}`], {
|
|
185
|
+
capture: true,
|
|
186
|
+
input: content,
|
|
187
|
+
}).stdout.trim()
|
|
188
|
+
runOrFail('git', ['update-index', '--add', '--cacheinfo', `100644,${sha},${file}`])
|
|
189
|
+
staged += 1
|
|
83
190
|
}
|
|
191
|
+
return staged
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let commitsMade = 0
|
|
195
|
+
|
|
196
|
+
function commit({ subject, body }) {
|
|
197
|
+
commitsMade += 1
|
|
198
|
+
console.log(`${TAG} Committing: ${subject}`)
|
|
199
|
+
runOrFail('git', body ? ['commit', '-m', subject, '-m', body] : ['commit', '-m', subject])
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Steps
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
84
205
|
|
|
85
|
-
|
|
86
|
-
|
|
206
|
+
function refreshLockfile(advisories) {
|
|
207
|
+
const names = [...new Set(advisories.map((advisory) => advisory.name))].sort()
|
|
208
|
+
console.log(`${TAG} Refreshing within existing ranges: ${names.join(', ')}`)
|
|
209
|
+
const recursive = isWorkspaceWithPackages() ? ['--recursive'] : []
|
|
210
|
+
runOrFail('pnpm', ['update', ...recursive, ...names, '--lockfile-only'])
|
|
211
|
+
}
|
|
87
212
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
213
|
+
/** Split planned overrides into ones we apply and ones held back by minimumReleaseAge. */
|
|
214
|
+
function applyReleaseAgeGate(overrides) {
|
|
215
|
+
const config = readPnpmConfig()
|
|
216
|
+
const minutes = Number(config.minimumReleaseAge ?? DEFAULT_MINIMUM_RELEASE_AGE_MINUTES)
|
|
217
|
+
if (!minutes) return { allowed: overrides, heldBack: [] }
|
|
218
|
+
const excluded = config.minimumReleaseAgeExclude ?? []
|
|
219
|
+
const allowed = []
|
|
220
|
+
const heldBack = []
|
|
221
|
+
const timesByName = new Map()
|
|
222
|
+
for (const override of overrides) {
|
|
223
|
+
if (isReleaseAgeExcluded(excluded, override.name, override.value)) {
|
|
224
|
+
allowed.push(override)
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
if (!timesByName.has(override.name)) timesByName.set(override.name, publishTimes(override.name))
|
|
228
|
+
const published = Date.parse(timesByName.get(override.name)[override.value] ?? '')
|
|
229
|
+
const ageMinutes = (Date.now() - published) / 60000
|
|
230
|
+
if (Number.isFinite(ageMinutes) && ageMinutes < minutes) {
|
|
231
|
+
heldBack.push({ ...override, waitHours: Math.ceil((minutes - ageMinutes) / 60) })
|
|
232
|
+
} else {
|
|
233
|
+
allowed.push(override)
|
|
234
|
+
}
|
|
91
235
|
}
|
|
236
|
+
return { allowed, heldBack }
|
|
237
|
+
}
|
|
92
238
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
239
|
+
function writeOverrides(overrides) {
|
|
240
|
+
const file = path.join(cwd, 'pnpm-workspace.yaml')
|
|
241
|
+
const before = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : ''
|
|
242
|
+
fs.writeFileSync(file, upsertWorkspaceOverrides(before, overrides))
|
|
243
|
+
for (const override of overrides) {
|
|
244
|
+
console.log(`${TAG} Override ${override.key}: ${override.value} (${override.ghsas.join(', ')})`)
|
|
96
245
|
}
|
|
246
|
+
runOrFail('pnpm', ['install', '--lockfile-only', '--ignore-scripts', '--no-frozen-lockfile'])
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function pruneOverrides() {
|
|
250
|
+
if (!/^overrides:/m.test(readFile('pnpm-workspace.yaml') ?? '')) return
|
|
251
|
+
console.log(`${TAG} Pruning overrides that no longer affect pnpm audit…`)
|
|
252
|
+
runOrFail(process.execPath, [path.join(scriptDir, 'prune-overrides.mjs'), '--fix'])
|
|
253
|
+
}
|
|
97
254
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
255
|
+
/**
|
|
256
|
+
* Write the repo's dependency-check-suppressions.xml with only the entries from fg-devkit's
|
|
257
|
+
* shared file that match a finding still reported here. Untouched when nothing matches.
|
|
258
|
+
*/
|
|
259
|
+
function syncSuppressions(remaining) {
|
|
260
|
+
const sourcePath = path.resolve(scriptDir, '..', 'configs', SUPPRESSIONS_FILE)
|
|
261
|
+
if (!fs.existsSync(sourcePath)) return { covered: [], uncovered: remaining }
|
|
262
|
+
const shared = fs.readFileSync(sourcePath, 'utf8')
|
|
263
|
+
const active = activeSuppressedGhsas(shared)
|
|
264
|
+
const covered = remaining
|
|
265
|
+
.filter((advisory) => active.has(advisory.ghsa))
|
|
266
|
+
.map((advisory) => ({ ...advisory, until: active.get(advisory.ghsa) }))
|
|
267
|
+
const uncovered = remaining.filter((advisory) => !active.has(advisory.ghsa))
|
|
268
|
+
if (covered.length === 0) return { covered, uncovered }
|
|
269
|
+
const content = filterSuppressions(
|
|
270
|
+
shared,
|
|
271
|
+
covered.map((advisory) => advisory.ghsa),
|
|
272
|
+
)
|
|
273
|
+
if (readFile(SUPPRESSIONS_FILE) !== content) {
|
|
274
|
+
fs.writeFileSync(path.join(cwd, SUPPRESSIONS_FILE), content)
|
|
275
|
+
suppressionsWritten = true
|
|
276
|
+
console.log(`${TAG} Wrote ${SUPPRESSIONS_FILE} with the entries that apply to this repo.`)
|
|
101
277
|
}
|
|
278
|
+
return { covered, uncovered }
|
|
279
|
+
}
|
|
102
280
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
281
|
+
function verify() {
|
|
282
|
+
console.log(`${TAG} Installing…`)
|
|
283
|
+
runOrFail('pnpm', ['install', '--no-frozen-lockfile'])
|
|
284
|
+
if (args['no-verify']) return
|
|
285
|
+
const scripts = JSON.parse(readFile('package.json')).scripts ?? {}
|
|
286
|
+
const toRun = VERIFY_SCRIPTS.filter((name) => scripts[name])
|
|
287
|
+
if (toRun.length === 0) {
|
|
288
|
+
console.log(`${TAG} No verification scripts (${VERIFY_SCRIPTS.join(', ')}) found; skipping checks.`)
|
|
289
|
+
return
|
|
109
290
|
}
|
|
291
|
+
for (const name of toRun) {
|
|
292
|
+
console.log(`${TAG} Verifying: pnpm run ${name}`)
|
|
293
|
+
runOrFail('pnpm', ['run', name], { env: { CI: 'true' } })
|
|
294
|
+
}
|
|
295
|
+
}
|
|
110
296
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
297
|
+
function printSummary({ fixedByRefresh, plan, heldBack, applied, remaining, suppressions }) {
|
|
298
|
+
const suppressed = new Set(suppressions.covered.map((advisory) => advisory.ghsa))
|
|
299
|
+
const unique = (items) => [...new Set(items)]
|
|
300
|
+
const lines = []
|
|
301
|
+
const section = (title, items) => {
|
|
302
|
+
if (items.length === 0) return
|
|
303
|
+
lines.push(title, ...items.map((item) => ` - ${item}`))
|
|
115
304
|
}
|
|
305
|
+
section('Fixed by lockfile refresh:', fixedByRefresh)
|
|
306
|
+
section(
|
|
307
|
+
'Overridden (parent range blocked the patch):',
|
|
308
|
+
applied.map((item) => `${item.key} -> ${item.value} (${item.ghsas.join(', ')})`),
|
|
309
|
+
)
|
|
310
|
+
section(
|
|
311
|
+
'Held back by minimumReleaseAge (re-run later, or add to minimumReleaseAgeExclude):',
|
|
312
|
+
heldBack.map((item) => `${item.name}@${item.value}, ~${item.waitHours}h left (${item.ghsas.join(', ')})`),
|
|
313
|
+
)
|
|
314
|
+
section(
|
|
315
|
+
'Direct dependency, bump it in package.json (policy-upgrade-commit):',
|
|
316
|
+
plan.direct.map((item) => `${item.name} ${item.versions.join(', ')} (${item.ghsas.join(', ')})`),
|
|
317
|
+
)
|
|
318
|
+
section(
|
|
319
|
+
'Patch only in a new major, upgrade the parent instead:',
|
|
320
|
+
plan.crossMajor.map((item) => `${item.name} ${item.from} -> ${item.to} (${item.ghsas.join(', ')})`),
|
|
321
|
+
)
|
|
322
|
+
section(
|
|
323
|
+
'No patched version and not suppressed, add a <suppress> entry to fg-devkit configs/dependency-check-suppressions.xml if the risk is accepted:',
|
|
324
|
+
plan.noPatch
|
|
325
|
+
.filter((item) => !item.ghsas.every((ghsa) => suppressed.has(ghsa)))
|
|
326
|
+
.map((item) => `${item.name} ${item.versions.join(', ')} (${item.ghsas.join(', ')})`),
|
|
327
|
+
)
|
|
328
|
+
section(
|
|
329
|
+
`Accepted in ${SUPPRESSIONS_FILE}${suppressionsWritten ? ' (updated from fg-devkit)' : ' (already up to date)'}:`,
|
|
330
|
+
unique(
|
|
331
|
+
suppressions.covered.map(
|
|
332
|
+
(advisory) => `${advisory.name} ${advisory.ghsa}${advisory.until ? `, until ${advisory.until}` : ''}`,
|
|
333
|
+
),
|
|
334
|
+
),
|
|
335
|
+
)
|
|
336
|
+
section(
|
|
337
|
+
'Could not read the patched range, check manually:',
|
|
338
|
+
plan.unparseable.map((item) => `${item.name}: ${item.patched} (${item.ghsas.join(', ')})`),
|
|
339
|
+
)
|
|
340
|
+
if (remaining.length > 0) {
|
|
341
|
+
lines.push(`Still reported by pnpm audit: ${remaining.length} advisory range(s).`)
|
|
342
|
+
} else {
|
|
343
|
+
lines.push('pnpm audit is clean.')
|
|
344
|
+
}
|
|
345
|
+
console.log(`\n${TAG} Summary\n${lines.join('\n')}`)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// Main
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
function main() {
|
|
353
|
+
if (!readFile('package.json')) throw new Error(`package.json not found in ${cwd}`)
|
|
354
|
+
if (!readFile('pnpm-lock.yaml')) throw new Error(`pnpm-lock.yaml not found in ${cwd}`)
|
|
355
|
+
const shouldCommit = !args['no-commit']
|
|
356
|
+
if (shouldCommit) assertCleanGitState()
|
|
357
|
+
|
|
358
|
+
const original = snapshot()
|
|
359
|
+
const overridesBefore = collectOverrides(cwd)
|
|
116
360
|
|
|
117
|
-
console.log(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
console.log(
|
|
361
|
+
console.log(`${TAG} Running pnpm audit…`)
|
|
362
|
+
const initial = audit()
|
|
363
|
+
if (initial.length === 0) {
|
|
364
|
+
console.log(`${TAG} pnpm audit is clean. Nothing to do.`)
|
|
365
|
+
return 0
|
|
121
366
|
}
|
|
367
|
+
console.log(`${TAG} ${initial.length} advisory range(s) in ${new Set(initial.map((a) => a.name)).size} package(s).`)
|
|
122
368
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
369
|
+
// 2. Refresh within ranges.
|
|
370
|
+
refreshLockfile(initial)
|
|
371
|
+
const refreshed = snapshot()
|
|
372
|
+
const refreshChanges = diffLockfileVersions(original['pnpm-lock.yaml'], refreshed['pnpm-lock.yaml'])
|
|
373
|
+
|
|
374
|
+
// 3. Plan overrides for what is left.
|
|
375
|
+
const afterRefresh = audit()
|
|
376
|
+
const remainingIds = new Set(afterRefresh.map((advisory) => advisory.id))
|
|
377
|
+
const fixedByRefresh = [
|
|
378
|
+
...new Set(initial.filter((advisory) => !remainingIds.has(advisory.id)).map((a) => `${a.name} (${a.ghsa})`)),
|
|
379
|
+
]
|
|
380
|
+
const plan = planOverrides(afterRefresh, { directDependencies: directDependencies() })
|
|
381
|
+
const { allowed, heldBack } = applyReleaseAgeGate(plan.overrides)
|
|
382
|
+
if (allowed.length > 0) writeOverrides(allowed)
|
|
383
|
+
|
|
384
|
+
// 4. Prune, 5. sync suppressions, 6. verify.
|
|
385
|
+
pruneOverrides()
|
|
386
|
+
const remaining = allowed.length > 0 ? audit() : afterRefresh
|
|
387
|
+
const suppressions = syncSuppressions(remaining)
|
|
388
|
+
printSummary({ fixedByRefresh, plan, heldBack, applied: allowed, remaining, suppressions })
|
|
389
|
+
verify()
|
|
390
|
+
|
|
391
|
+
// 7. Commit.
|
|
392
|
+
const final = snapshot()
|
|
393
|
+
const overrideChanges = collectOverrideChanges(overridesBefore, collectOverrides(cwd))
|
|
394
|
+
const nothingChanged = !suppressionsWritten && TRACKED_FILES.every((file) => final[file] === original[file])
|
|
395
|
+
if (nothingChanged) {
|
|
396
|
+
console.log(`${TAG} No file changes.`)
|
|
397
|
+
return 0
|
|
398
|
+
}
|
|
399
|
+
if (!shouldCommit) {
|
|
400
|
+
console.log(`${TAG} --no-commit: changes left in the working tree.`)
|
|
401
|
+
return 0
|
|
126
402
|
}
|
|
127
403
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (body) {
|
|
131
|
-
console.log(body)
|
|
404
|
+
if (refreshChanges.length > 0 && stageContents(refreshed) > 0) {
|
|
405
|
+
commit(buildRefreshCommitMessage(refreshChanges))
|
|
132
406
|
}
|
|
133
|
-
|
|
134
|
-
|
|
407
|
+
if (overrideChanges.length > 0) {
|
|
408
|
+
for (const item of overrideChanges) {
|
|
409
|
+
console.log(` - ${item.name}: ${item.from ?? '(missing)'} -> ${formatOverrideValue(item.to)}`)
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (stageContents(final) > 0) {
|
|
413
|
+
commit(
|
|
414
|
+
overrideChanges.length > 0
|
|
415
|
+
? buildOverrideCommitMessage(overrideChanges)
|
|
416
|
+
: buildRefreshCommitMessage(diffLockfileVersions(refreshed['pnpm-lock.yaml'], final['pnpm-lock.yaml'])),
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
if (suppressionsWritten) {
|
|
420
|
+
runOrFail('git', ['add', '--', SUPPRESSIONS_FILE])
|
|
421
|
+
commit({ subject: SUPPRESSIONS_COMMIT_MESSAGE })
|
|
422
|
+
}
|
|
423
|
+
return 0
|
|
135
424
|
}
|
|
136
425
|
|
|
137
|
-
|
|
426
|
+
try {
|
|
427
|
+
process.exit(main())
|
|
428
|
+
} catch (error) {
|
|
429
|
+
console.error(`${TAG} ${error instanceof Error ? error.message : error}`)
|
|
430
|
+
console.error(
|
|
431
|
+
commitsMade > 0
|
|
432
|
+
? `${TAG} ${commitsMade} commit(s) were made before the failure. Check git log and git status.`
|
|
433
|
+
: `${TAG} Nothing was committed. Inspect the changes with git status and git diff.`,
|
|
434
|
+
)
|
|
435
|
+
process.exit(1)
|
|
436
|
+
}
|
package/scripts/fg-devkit.mjs
CHANGED
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
* pnpm exec fg-devkit bump-node-toolchain [nvmrc-version] [projects-root] [--dry-run] [--skip-fleet] [--no-engines]
|
|
17
17
|
* pnpm exec fg-devkit policy-upgrade [policy-file] [ncu-args...]
|
|
18
18
|
* pnpm exec fg-devkit policy-upgrade-commit [--prune-overrides] [ncu-args...]
|
|
19
|
-
* pnpm exec fg-devkit audit-override-commit [
|
|
19
|
+
* pnpm exec fg-devkit audit-override-commit [--no-verify] [--no-commit]
|
|
20
20
|
* pnpm exec fg-devkit prune-overrides [--fix] [--commit]
|
|
21
21
|
* pnpm exec fg-devkit sync-project-configs (VS Code + tooling sync; overwrites config files)
|
|
22
22
|
* pnpm exec fg-devkit sync-tooling-configs (tooling sync only; overwrites config files)
|
|
23
|
-
* pnpm exec fg-devkit sync-dependency-check-suppressions-commit
|
|
23
|
+
* pnpm exec fg-devkit sync-dependency-check-suppressions-commit [--all]
|
|
24
24
|
* pnpm exec fg-devkit sync-vscode-settings
|
|
25
25
|
* pnpm exec fg-devkit sync-core-coding-standards
|
|
26
26
|
* pnpm exec fg-devkit sync-consumer-scripts
|
|
@@ -542,14 +542,14 @@ Commands:
|
|
|
542
542
|
policy-upgrade-commit [--prune-overrides] [ncu-args...]
|
|
543
543
|
Upgrade consumer package.json packages and auto-commit
|
|
544
544
|
--prune-overrides also drops unused workspace overrides in a second commit
|
|
545
|
-
audit-override-commit [
|
|
546
|
-
Fix
|
|
545
|
+
audit-override-commit [--no-verify] [--no-commit]
|
|
546
|
+
Fix pnpm audit findings: refresh lockfile first, override only when a range blocks the fix, verify, auto-commit
|
|
547
547
|
prune-overrides [--fix] [--commit]
|
|
548
548
|
Report unused workspace overrides; --fix removes them; --commit also auto-commits
|
|
549
549
|
sync-project-configs VS Code + tooling sync (overwrites config files)
|
|
550
550
|
sync-tooling-configs Tooling sync only (overwrites config files)
|
|
551
|
-
sync-dependency-check-suppressions-commit
|
|
552
|
-
|
|
551
|
+
sync-dependency-check-suppressions-commit [--all]
|
|
552
|
+
Write the dependency-check suppressions that match pnpm audit findings and auto-commit (--all: copy every entry)
|
|
553
553
|
sync-vscode-settings Copy shared VS Code settings into .vscode/settings.json
|
|
554
554
|
sync-core-coding-standards Copy .cursor core coding standards with consumer package name
|
|
555
555
|
sync-vite-plugin-vue-inspector Add/pin vite-plugin-vue-inspector, wire vite.config, install, commit
|