@dependfix/engine 0.5.0 → 0.6.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/dist/chunks/auth.mjs +5 -10
- package/dist/chunks/auth.mjs.map +1 -1
- package/dist/chunks/index.d.mts +0 -8
- package/dist/index.d.mts +92 -130
- package/dist/index.mjs +1179 -650
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { a as fromPat, c as computeRetryDelayMs, l as createGitHubClient, r as fromApp } from "./chunks/auth.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { RequestError } from "@octokit/request-error";
|
|
4
|
-
import { AppError, addSkipped, addTruncatedBySeverity, aggregateCheckAttribution, classifyCheckFailure, collectCodeScanningSuggestions, collectFamilySummaries, countUnfixedAlerts, createEmptyRunSummary, createLogger, filterAlerts, formatCiAlignmentSection, formatSeverity, formatSkipReason, generateJsonReport, generateMarkdownReport, isAlertFixedByActions, isFailedConclusion, isValidRepoIdentifier, limitAlerts, mapCodeScanningSeverity, normalizeAuditSeverity, normalizeUpstreamId, prioritizeAlerts, skippedBreakdown, toErrorMessage, truncatedBreakdown, writeReport } from "@dependfix/core";
|
|
4
|
+
import { AppError, addSkipped, addTruncatedBySeverity, aggregateCheckAttribution, alertOutcomeKey, buildAlertOutcomes, checkRoundKey, classifyCheckFailure, collectCodeScanningSuggestions, collectFamilySummaries, countUnfixedAlerts, createEmptyRunSummary, createLogger, dedupeByLatestRound, filterAlerts, formatCiAlignmentSection, formatSeverity, formatSkipReason, generateJsonReport, generateMarkdownReport, isAlertFixedByActions, isFailedConclusion, isValidRepoIdentifier, limitAlerts, mapCodeScanningSeverity, normalizeAuditSeverity, normalizeUpstreamId, prioritizeAlerts, resolveCheckProvider, resolveStatusProvider, skippedBreakdown, toErrorMessage, truncatedBreakdown, writeReport } from "@dependfix/core";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
|
-
import { execFileSync, execSync, spawn, spawnSync } from "node:child_process";
|
|
7
6
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
|
+
import { execFileSync, execSync, spawn, spawnSync } from "node:child_process";
|
|
8
|
+
import { StringDecoder } from "node:string_decoder";
|
|
8
9
|
import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
9
10
|
import YAML, { isMap, isScalar, parse } from "yaml";
|
|
10
11
|
import PQueue from "p-queue";
|
|
@@ -14,7 +15,7 @@ import { z } from "zod";
|
|
|
14
15
|
import { createInterface } from "node:readline";
|
|
15
16
|
import { compare, prerelease, satisfies, valid, validRange } from "semver";
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
// src/github/fix-fingerprint.ts
|
|
18
19
|
/** 尝试键长度(hex 字符数)。 */
|
|
19
20
|
const ATTEMPT_KEY_LENGTH = 16;
|
|
20
21
|
/** 失败签名归一化输入上限(字符数;失败命令 + 错误文本各截断到该长度)。 */
|
|
@@ -163,8 +164,7 @@ function parseMarkerAttributes(input) {
|
|
|
163
164
|
return attributes;
|
|
164
165
|
}
|
|
165
166
|
|
|
166
|
-
|
|
167
|
-
//#region src/github/git-signing.ts
|
|
167
|
+
// src/github/git-signing.ts
|
|
168
168
|
/**
|
|
169
169
|
* git 执行语义隔离参数(单一事实源,engine 与平台侧共用)。
|
|
170
170
|
*
|
|
@@ -213,8 +213,326 @@ const GIT_PUSH_SIGNING_ISOLATION_ARGS = ["-c", "push.gpgSign=false"];
|
|
|
213
213
|
*/
|
|
214
214
|
const GIT_PUSH_HOOKS_ISOLATION_ARGS = ["--no-verify"];
|
|
215
215
|
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
// src/runners/process-control.ts
|
|
217
|
+
/** 单流捕获上限(与 `execSync` 默认 `maxBuffer` 同量级):超限后继续 drain 但不再累积。 */
|
|
218
|
+
const MAX_STREAM_CAPTURE_BYTES = 1048576;
|
|
219
|
+
/**
|
|
220
|
+
* 子进程退出后等待 stdio 关闭的有界宽限期。
|
|
221
|
+
*
|
|
222
|
+
* `close` 事件只有在**所有**持有 stdout / stderr 写端的进程都退出后才会触发;安装脚本
|
|
223
|
+
* (node-gyp / prebuild-install / postinstall)可能派生出脱离直接子进程生命周期的孙进程,
|
|
224
|
+
* 即使进程组已被终止,只要孙进程仍持有管道,`close` 就永远不会到来。实测一次 5 分钟
|
|
225
|
+
* 安装超时因此拖到 12 分钟才落定,直接吃掉单仓 30 分钟执行预算。
|
|
226
|
+
*
|
|
227
|
+
* 超时只约束"等待退出",不改变输出语义:宽限期内正常关闭仍走 `close`;超期则主动
|
|
228
|
+
* destroy 管道并用已捕获的输出落定。
|
|
229
|
+
*/
|
|
230
|
+
const STREAM_CLOSE_GRACE_MS = 2e3;
|
|
231
|
+
/** 运行级 abort 原因(`undefined` = 未请求取消)。 */
|
|
232
|
+
let abortReason;
|
|
233
|
+
/** 已登记的活跃子进程 PID(超时 / abort 时按进程树终止)。 */
|
|
234
|
+
const trackedPids = /* @__PURE__ */ new Set();
|
|
235
|
+
/**
|
|
236
|
+
* 复位运行级进程控制状态(每次构造 `DependfixApp` 时调用)。
|
|
237
|
+
*
|
|
238
|
+
* 只清状态、**不杀进程**:复位发生在两次运行之间,此时上一次运行的子进程应由其调用方
|
|
239
|
+
* 正常收尾(`runTrackedCommand` 在 close / error 时已注销)。
|
|
240
|
+
*/
|
|
241
|
+
function resetProcessControlState() {
|
|
242
|
+
abortReason = void 0;
|
|
243
|
+
trackedPids.clear();
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* 请求运行级取消(幂等)。
|
|
247
|
+
*
|
|
248
|
+
* 首次调用记录原因;**每次调用都终止当前已登记的子进程树**——这样在"平台放弃等待"之后,
|
|
249
|
+
* 正在跑的 install / 验证命令会被立即杀掉,而不是继续与新仓库争抢资源。
|
|
250
|
+
*/
|
|
251
|
+
function requestAbort(reason) {
|
|
252
|
+
if (abortReason === void 0) abortReason = reason ?? "abort requested";
|
|
253
|
+
killAllTrackedChildProcesses();
|
|
254
|
+
}
|
|
255
|
+
/** 是否已请求取消(调用方在启动新命令 / 新仓库之前短路)。 */
|
|
256
|
+
function isAbortRequested() {
|
|
257
|
+
return abortReason !== void 0;
|
|
258
|
+
}
|
|
259
|
+
/** 取消原因(未取消时为 `undefined`)。 */
|
|
260
|
+
function getAbortReason() {
|
|
261
|
+
return abortReason;
|
|
262
|
+
}
|
|
263
|
+
/** 登记活跃子进程 PID(无 PID / 非法 PID 静默忽略——如 spawn 立即失败)。 */
|
|
264
|
+
function trackChildProcess(pid) {
|
|
265
|
+
if (typeof pid === "number" && pid > 0) trackedPids.add(pid);
|
|
266
|
+
}
|
|
267
|
+
/** 注销活跃子进程 PID(进程已退出)。 */
|
|
268
|
+
function untrackChildProcess(pid) {
|
|
269
|
+
if (typeof pid === "number" && pid > 0) trackedPids.delete(pid);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* 终止所有已登记的子进程树,返回终止数量。
|
|
273
|
+
*
|
|
274
|
+
* 终止后立即清空登记表:再次调用不会重复 kill(PID 可能已被系统复用)。
|
|
275
|
+
*/
|
|
276
|
+
function killAllTrackedChildProcesses() {
|
|
277
|
+
let killed = 0;
|
|
278
|
+
for (const pid of [...trackedPids]) {
|
|
279
|
+
killProcessTree(pid);
|
|
280
|
+
trackedPids.delete(pid);
|
|
281
|
+
killed++;
|
|
282
|
+
}
|
|
283
|
+
return killed;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* 终止进程树:POSIX 杀进程组(spawn 时 `detached` 使子进程成为进程组组长),
|
|
287
|
+
* Windows 用 `taskkill /T /F`。失败时静默退化(调用方另有直接 `kill` 兜底)。
|
|
288
|
+
*
|
|
289
|
+
* 单一事实源:verification-runner 的超时终止与本模块的 abort 终止共用此实现。
|
|
290
|
+
*/
|
|
291
|
+
function killProcessTree(pid) {
|
|
292
|
+
if (pid === void 0 || pid <= 0) return;
|
|
293
|
+
if (process.platform === "win32") try {
|
|
294
|
+
spawnSync("taskkill", [
|
|
295
|
+
"/pid",
|
|
296
|
+
String(pid),
|
|
297
|
+
"/T",
|
|
298
|
+
"/F"
|
|
299
|
+
], { stdio: "ignore" });
|
|
300
|
+
} catch {}
|
|
301
|
+
else try {
|
|
302
|
+
process.kill(-pid, "SIGKILL");
|
|
303
|
+
} catch {}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* 是否经 shell 执行(Windows 需要;见模块头「平台差异」)。
|
|
307
|
+
*
|
|
308
|
+
* 抽成纯函数以便用例锁定平台分支:仅 `win32` 返回 true,其余平台 false
|
|
309
|
+
* (POSIX 保持直接 `spawn`,与改造前行为一致)。
|
|
310
|
+
*/
|
|
311
|
+
function resolveShellOption(platform = process.platform) {
|
|
312
|
+
return platform === "win32";
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* 异步执行命令并登记子进程(超时 / abort 时按进程树终止)。
|
|
316
|
+
*
|
|
317
|
+
* **取消前置守卫**:调用时若已请求取消,直接返回 `started: false`(不 spawn)——这是
|
|
318
|
+
* "abort 后不再启动新工作"在安装 / 验证类命令上的**单一入口**(各调用方无需各写一遍)。
|
|
319
|
+
*
|
|
320
|
+
* 与旧 `execFileSync` 的差异(有意为之):
|
|
321
|
+
* - **不阻塞事件循环**——引擎与平台同进程运行,同步调用会让平台 HTTP、定时器(含执行超时)
|
|
322
|
+
* 与队列全部停摆(实测:超时定时器比设定值晚约 9.5 分钟才触发)
|
|
323
|
+
* - **输出限长累积**:超过 `MAX_STREAM_CAPTURE_BYTES` 后继续 drain(避免子进程写满管道阻塞)
|
|
324
|
+
* 但不再累积,防大输出把内存吃掉
|
|
325
|
+
* - 不抛错:退出码 / 信号 / 超时 / errno 全部经返回值暴露,由调用方决定语义
|
|
326
|
+
*/
|
|
327
|
+
function runTrackedCommand(params) {
|
|
328
|
+
if (isAbortRequested()) return Promise.resolve({
|
|
329
|
+
started: false,
|
|
330
|
+
exitCode: null,
|
|
331
|
+
signal: null,
|
|
332
|
+
timedOut: false,
|
|
333
|
+
aborted: true,
|
|
334
|
+
errno: null,
|
|
335
|
+
stdout: "",
|
|
336
|
+
stderr: "",
|
|
337
|
+
durationMs: 0
|
|
338
|
+
});
|
|
339
|
+
return new Promise((resolve) => {
|
|
340
|
+
const startedAt = Date.now();
|
|
341
|
+
let timedOut = false;
|
|
342
|
+
const stdoutChunks = [];
|
|
343
|
+
const stderrChunks = [];
|
|
344
|
+
const stdoutDecoder = new StringDecoder("utf-8");
|
|
345
|
+
const stderrDecoder = new StringDecoder("utf-8");
|
|
346
|
+
let stdoutBytes = 0;
|
|
347
|
+
let stderrBytes = 0;
|
|
348
|
+
let outputTruncated = false;
|
|
349
|
+
const cp = spawn(params.binary, params.args, {
|
|
350
|
+
cwd: params.cwd,
|
|
351
|
+
stdio: "pipe",
|
|
352
|
+
shell: params.shell ?? resolveShellOption(),
|
|
353
|
+
detached: process.platform !== "win32",
|
|
354
|
+
...params.env ? { env: params.env } : {}
|
|
355
|
+
});
|
|
356
|
+
trackChildProcess(cp.pid);
|
|
357
|
+
let settled = false;
|
|
358
|
+
let exited = false;
|
|
359
|
+
let closeGraceTimer;
|
|
360
|
+
const timer = setTimeout(() => {
|
|
361
|
+
timedOut = true;
|
|
362
|
+
killProcessTree(cp.pid);
|
|
363
|
+
cp.kill("SIGKILL");
|
|
364
|
+
settleAfterStreamGrace({
|
|
365
|
+
exitCode: null,
|
|
366
|
+
signal: "SIGKILL"
|
|
367
|
+
});
|
|
368
|
+
}, params.timeoutMs);
|
|
369
|
+
function finish(result) {
|
|
370
|
+
if (settled) return;
|
|
371
|
+
settled = true;
|
|
372
|
+
clearTimeout(timer);
|
|
373
|
+
if (closeGraceTimer) clearTimeout(closeGraceTimer);
|
|
374
|
+
untrackChildProcess(cp.pid);
|
|
375
|
+
if (!outputTruncated) {
|
|
376
|
+
stdoutChunks.push(stdoutDecoder.end());
|
|
377
|
+
stderrChunks.push(stderrDecoder.end());
|
|
378
|
+
}
|
|
379
|
+
resolve({
|
|
380
|
+
started: true,
|
|
381
|
+
exitCode: result.exitCode,
|
|
382
|
+
signal: result.signal,
|
|
383
|
+
timedOut,
|
|
384
|
+
terminationUnconfirmed: timedOut && !exited ? true : void 0,
|
|
385
|
+
aborted: isAbortRequested(),
|
|
386
|
+
errno: result.errno ?? null,
|
|
387
|
+
stdout: stdoutChunks.join(""),
|
|
388
|
+
stderr: stderrChunks.join(""),
|
|
389
|
+
...outputTruncated ? { outputTruncated: true } : {},
|
|
390
|
+
durationMs: Date.now() - startedAt
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* 直接子进程已退出(或已被主动终止)后,等待 stdio 关闭的**有界**窗口。
|
|
395
|
+
* `close` 正常到来时由 `finish` 清掉本定时器;孙进程持有管道时由本定时器兜底。
|
|
396
|
+
*/
|
|
397
|
+
function settleAfterStreamGrace(result) {
|
|
398
|
+
if (settled) return;
|
|
399
|
+
if (closeGraceTimer) clearTimeout(closeGraceTimer);
|
|
400
|
+
closeGraceTimer = setTimeout(() => {
|
|
401
|
+
killProcessTree(cp.pid);
|
|
402
|
+
cp.stdout?.destroy?.();
|
|
403
|
+
cp.stderr?.destroy?.();
|
|
404
|
+
finish(result);
|
|
405
|
+
}, STREAM_CLOSE_GRACE_MS);
|
|
406
|
+
}
|
|
407
|
+
const capture = (decoder, chunks, bytes, data) => {
|
|
408
|
+
const next = bytes + data.byteLength;
|
|
409
|
+
if (bytes < MAX_STREAM_CAPTURE_BYTES) chunks.push(decoder.write(data));
|
|
410
|
+
else if (!outputTruncated) {
|
|
411
|
+
outputTruncated = true;
|
|
412
|
+
decoder.end();
|
|
413
|
+
}
|
|
414
|
+
return next;
|
|
415
|
+
};
|
|
416
|
+
cp.stdout?.on("data", (data) => {
|
|
417
|
+
stdoutBytes = capture(stdoutDecoder, stdoutChunks, stdoutBytes, data);
|
|
418
|
+
});
|
|
419
|
+
cp.stderr?.on("data", (data) => {
|
|
420
|
+
stderrBytes = capture(stderrDecoder, stderrChunks, stderrBytes, data);
|
|
421
|
+
});
|
|
422
|
+
cp.on("error", (err) => {
|
|
423
|
+
finish({
|
|
424
|
+
exitCode: null,
|
|
425
|
+
signal: null,
|
|
426
|
+
errno: err.code ?? "UNKNOWN"
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
cp.on("exit", (code, signal) => {
|
|
430
|
+
exited = true;
|
|
431
|
+
settleAfterStreamGrace({
|
|
432
|
+
exitCode: code,
|
|
433
|
+
signal
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
cp.on("close", (code, signal) => {
|
|
437
|
+
finish({
|
|
438
|
+
exitCode: code,
|
|
439
|
+
signal
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/github/git-run.ts
|
|
446
|
+
/** 交付类 git 命令的默认上限(弱网 push 的主要风险面;本地命令通常毫秒级结束)。 */
|
|
447
|
+
const GIT_COMMAND_TIMEOUT_MS = 3e5;
|
|
448
|
+
/**
|
|
449
|
+
* 错误信息里的命令展示:跳过 `-c key=value` 隔离参数,取真实子命令(如 `git push`)。
|
|
450
|
+
*
|
|
451
|
+
* 交付命令普遍带 `-c push.gpgSign=false` 之类的隔离项,直接取 `args[0]` 会渲染成 `git -c`。
|
|
452
|
+
*/
|
|
453
|
+
function describeGitCommand(args) {
|
|
454
|
+
const parts = [];
|
|
455
|
+
for (let index = 0; index < args.length; index++) {
|
|
456
|
+
const arg = args[index];
|
|
457
|
+
if (arg === "-c" || arg === "--config") {
|
|
458
|
+
index += 1;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
parts.push(arg);
|
|
462
|
+
if (parts.length >= 2) break;
|
|
463
|
+
}
|
|
464
|
+
return `git ${parts.join(" ")}`;
|
|
465
|
+
}
|
|
466
|
+
/** 失败原因的一句话描述(超时 / 取消前置守卫 / 退出码)。 */
|
|
467
|
+
function describeFailure(params) {
|
|
468
|
+
if (params.timedOut) return "timed out";
|
|
469
|
+
if (params.aborted && !params.started) return "aborted before start";
|
|
470
|
+
if (params.aborted) return "terminated by abort";
|
|
471
|
+
return `exit code ${params.code}`;
|
|
472
|
+
}
|
|
473
|
+
/** git 命令失败(非 0 退出 / 超时 / 未启动)的统一错误。 */
|
|
474
|
+
var GitCommandError = class extends Error {
|
|
475
|
+
args;
|
|
476
|
+
code;
|
|
477
|
+
timedOut;
|
|
478
|
+
aborted;
|
|
479
|
+
/** 是否真正启动过子进程(`false` = 取消前置守卫拦下) */
|
|
480
|
+
started;
|
|
481
|
+
stderr;
|
|
482
|
+
constructor(params) {
|
|
483
|
+
const detail = describeFailure(params);
|
|
484
|
+
const tail = params.stderr.trim().slice(-400);
|
|
485
|
+
super(`${params.what} failed: ${describeGitCommand(params.args)} — ${detail}${tail ? `: ${tail}` : ""}`);
|
|
486
|
+
this.name = "GitCommandError";
|
|
487
|
+
this.args = params.args;
|
|
488
|
+
this.code = params.code;
|
|
489
|
+
this.timedOut = params.timedOut;
|
|
490
|
+
this.aborted = params.aborted;
|
|
491
|
+
this.started = params.started;
|
|
492
|
+
this.stderr = params.stderr;
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
/** 执行 git 命令(不抛错;退出码 / 超时 / 取消经返回值暴露)。 */
|
|
496
|
+
async function runGit(args, options) {
|
|
497
|
+
const outcome = await runTrackedCommand({
|
|
498
|
+
binary: options.binary ?? "git",
|
|
499
|
+
args: [...args],
|
|
500
|
+
cwd: options.cwd,
|
|
501
|
+
timeoutMs: options.timeoutMs ?? 3e5,
|
|
502
|
+
shell: false,
|
|
503
|
+
...options.env ? { env: options.env } : {}
|
|
504
|
+
});
|
|
505
|
+
return {
|
|
506
|
+
stdout: outcome.stdout,
|
|
507
|
+
stderr: outcome.stderr,
|
|
508
|
+
code: outcome.exitCode,
|
|
509
|
+
timedOut: outcome.timedOut,
|
|
510
|
+
aborted: outcome.aborted,
|
|
511
|
+
started: outcome.started,
|
|
512
|
+
truncated: outcome.outputTruncated === true
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* 执行 git 命令并要求成功(非 0 退出 / 超时 / 取消前置守卫 → 抛 {@link GitCommandError})。
|
|
517
|
+
*
|
|
518
|
+
* @param what 业务动作名(错误信息前缀,如 `checkout fix branch`)
|
|
519
|
+
* @returns 标准输出(已 trim 的换行由调用方决定)
|
|
520
|
+
*/
|
|
521
|
+
async function runGitOrThrow(args, options) {
|
|
522
|
+
const result = await runGit(args, options);
|
|
523
|
+
if (!result.started || result.code !== 0) throw new GitCommandError({
|
|
524
|
+
what: options.what,
|
|
525
|
+
args,
|
|
526
|
+
code: result.code,
|
|
527
|
+
timedOut: result.timedOut,
|
|
528
|
+
aborted: result.aborted || !result.started,
|
|
529
|
+
started: result.started,
|
|
530
|
+
stderr: result.stderr
|
|
531
|
+
});
|
|
532
|
+
return result.stdout;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/github/commit-scope.ts
|
|
218
536
|
var CommitScopeError = class extends Error {
|
|
219
537
|
category;
|
|
220
538
|
paths;
|
|
@@ -311,8 +629,8 @@ function normalizeCommitPath(raw) {
|
|
|
311
629
|
* - `--no-renames`:重命名拆成「删除 + 新增」,删除因此不会绕过未声明检测
|
|
312
630
|
* - 输出路径相对**仓库根**:按 `--show-prefix` 折算为 workDir 相对路径(workDir 可为子目录)
|
|
313
631
|
*/
|
|
314
|
-
function listChangedPaths(workDir) {
|
|
315
|
-
const raw =
|
|
632
|
+
async function listChangedPaths(workDir) {
|
|
633
|
+
const raw = await runGitOrThrow([
|
|
316
634
|
"status",
|
|
317
635
|
"--porcelain=v1",
|
|
318
636
|
"-z",
|
|
@@ -320,14 +638,12 @@ function listChangedPaths(workDir) {
|
|
|
320
638
|
"--no-renames"
|
|
321
639
|
], {
|
|
322
640
|
cwd: workDir,
|
|
323
|
-
|
|
324
|
-
encoding: "utf-8"
|
|
641
|
+
what: "list changed paths"
|
|
325
642
|
});
|
|
326
|
-
const prefix =
|
|
643
|
+
const prefix = (await runGitOrThrow(["rev-parse", "--show-prefix"], {
|
|
327
644
|
cwd: workDir,
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}).trim();
|
|
645
|
+
what: "resolve repository prefix"
|
|
646
|
+
})).trim();
|
|
331
647
|
const changes = [];
|
|
332
648
|
for (const entry of raw.split("\0")) {
|
|
333
649
|
if (!entry) continue;
|
|
@@ -367,21 +683,21 @@ function collectDeclaredPaths(actions) {
|
|
|
367
683
|
* 索引区越界暂存内容(`UNEXPECTED_CHANGES`)。暂存用 `git add -- <paths>`(分批;`--` 阻断选项解析,
|
|
368
684
|
* 每条路径加 `:(literal)` 终止通配,文件名含 `*` / `?` / `[` 时也只暂存字面路径)。
|
|
369
685
|
*/
|
|
370
|
-
function stageScopedChanges(workDir, declaredPaths = []) {
|
|
371
|
-
const scope = computeCommitScope(workDir, declaredPaths);
|
|
686
|
+
async function stageScopedChanges(workDir, declaredPaths = []) {
|
|
687
|
+
const scope = await computeCommitScope(workDir, declaredPaths);
|
|
372
688
|
if (scope.conflicts.length > 0) throw new CommitScopeError("UNEXPECTED_CHANGES", `refusing to commit: unresolved merge conflict(s) — ${scope.conflicts.join(", ")}. Resolve them before running dependfix.`, scope.conflicts);
|
|
373
689
|
if (scope.rejected.length > 0) throw new CommitScopeError("UNEXPECTED_CHANGES", `refusing to commit: invalid declared path(s) — ${scope.rejected.map((item) => `${item.path} (${item.reason})`).join("; ")}`, scope.rejected.map((item) => item.path));
|
|
374
690
|
if (scope.unexpected.length > 0) throw new CommitScopeError("UNEXPECTED_CHANGES", `refusing to commit: ${scope.unexpected.length} undeclared tracked change(s) — ${scope.unexpected.join(", ")}. Only whitelisted manifests/lockfile/workspace files and fixer-declared paths are committed; commit or clean these changes first.`, scope.unexpected);
|
|
375
691
|
const outOfScopeStaged = scope.staged.filter((path) => !scope.paths.includes(path));
|
|
376
692
|
if (outOfScopeStaged.length > 0) throw new CommitScopeError("UNEXPECTED_CHANGES", `refusing to commit: index already contains ${outOfScopeStaged.length} staged change(s) outside the commit scope — ${outOfScopeStaged.join(", ")}. Unstage them (git restore --staged) before running dependfix.`, outOfScopeStaged);
|
|
377
693
|
if (scope.paths.length === 0) throw new CommitScopeError("NO_CHANGES", "nothing to commit: the working tree contains only generated or undeclared untracked files");
|
|
378
|
-
for (const chunk of chunkPaths(scope.paths))
|
|
694
|
+
for (const chunk of chunkPaths(scope.paths)) await runGitOrThrow([
|
|
379
695
|
"add",
|
|
380
696
|
"--",
|
|
381
697
|
...chunk.map((path) => `:(literal)${path}`)
|
|
382
698
|
], {
|
|
383
699
|
cwd: workDir,
|
|
384
|
-
|
|
700
|
+
what: "stage commit scope"
|
|
385
701
|
});
|
|
386
702
|
return {
|
|
387
703
|
staged: scope.paths,
|
|
@@ -395,8 +711,8 @@ function stageScopedChanges(workDir, declaredPaths = []) {
|
|
|
395
711
|
* author / 签名 / hooks 三条隔离:显式 `-c user.name` / `-c user.email` 覆盖 host 全局,
|
|
396
712
|
* `-c commit.gpgsign=false` 阻断宿主签名,`--no-verify` 跳过目标仓库钩子(详见 ./git-signing.ts)。
|
|
397
713
|
*/
|
|
398
|
-
function commitStaged(message, workDir, author) {
|
|
399
|
-
|
|
714
|
+
async function commitStaged(message, workDir, author) {
|
|
715
|
+
await runGitOrThrow([
|
|
400
716
|
"-c",
|
|
401
717
|
`user.name=${author.name}`,
|
|
402
718
|
"-c",
|
|
@@ -408,11 +724,11 @@ function commitStaged(message, workDir, author) {
|
|
|
408
724
|
message
|
|
409
725
|
], {
|
|
410
726
|
cwd: workDir,
|
|
411
|
-
|
|
727
|
+
what: "commit fix changes"
|
|
412
728
|
});
|
|
413
729
|
}
|
|
414
730
|
/** 判定本次提交集合(不写盘、不暂存)。 */
|
|
415
|
-
function computeCommitScope(workDir, declaredPaths = []) {
|
|
731
|
+
async function computeCommitScope(workDir, declaredPaths = []) {
|
|
416
732
|
const rejected = [];
|
|
417
733
|
const declared = /* @__PURE__ */ new Set();
|
|
418
734
|
const conflicts = [];
|
|
@@ -424,7 +740,7 @@ function computeCommitScope(workDir, declaredPaths = []) {
|
|
|
424
740
|
reason: result.reason
|
|
425
741
|
});
|
|
426
742
|
}
|
|
427
|
-
const changes = listChangedPaths(workDir);
|
|
743
|
+
const changes = await listChangedPaths(workDir);
|
|
428
744
|
const paths = /* @__PURE__ */ new Set();
|
|
429
745
|
const unexpected = [];
|
|
430
746
|
const ignored = [];
|
|
@@ -483,8 +799,7 @@ function hasControlChar(value) {
|
|
|
483
799
|
return false;
|
|
484
800
|
}
|
|
485
801
|
|
|
486
|
-
|
|
487
|
-
//#region src/github/markdown-table.ts
|
|
802
|
+
// src/github/markdown-table.ts
|
|
488
803
|
/**
|
|
489
804
|
* 转义 Markdown 表格单元格:`\` 先转义再转义 `|`(避免已有转义双重处理),换行折叠为空格。
|
|
490
805
|
*/
|
|
@@ -492,21 +807,171 @@ function escapeTableCell(value) {
|
|
|
492
807
|
return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
493
808
|
}
|
|
494
809
|
|
|
495
|
-
|
|
496
|
-
|
|
810
|
+
// src/github/delivery-issue.ts
|
|
811
|
+
/** 交付跳过 issue 的推荐标签(仅用于检索;仓库未建该标签时可能被静默丢弃,去重不依赖它)。 */
|
|
812
|
+
const DELIVERY_SKIP_LABEL = "dependfix";
|
|
813
|
+
/** 交付跳过 issue 的 HTML 标记前缀(正文首位;去重判定用)。 */
|
|
814
|
+
const DELIVERY_SKIP_MARKER_PREFIX = "<!-- dependfix:delivery-skip";
|
|
815
|
+
/**
|
|
816
|
+
* 计算交付失败指纹(sha256 前 16 位)。
|
|
817
|
+
*
|
|
818
|
+
* 输入顺序无关(仓库集 / 命令集分别排序后拼接),刻意不含错误文本与时间戳:
|
|
819
|
+
* 同仓库、同失败命令即视为同一失败,issue 关闭后不再重开。
|
|
820
|
+
*/
|
|
821
|
+
function computeDeliverySkipFingerprint(input) {
|
|
822
|
+
const repositories = [...new Set(input.repositories)].sort().join("\0");
|
|
823
|
+
const commands = [...new Set(input.failingCommands)].sort().join("\0");
|
|
824
|
+
return createHash("sha256").update(`${repositories}\u0001${commands}`).digest("hex").slice(0, 16);
|
|
825
|
+
}
|
|
826
|
+
/** 生成 issue 正文头部的去重标记。 */
|
|
827
|
+
function formatDeliverySkipMarker(fingerprint) {
|
|
828
|
+
return `${DELIVERY_SKIP_MARKER_PREFIX} fp=${fingerprint} -->`;
|
|
829
|
+
}
|
|
830
|
+
/** 正文是否携带指定指纹的交付跳过标记。 */
|
|
831
|
+
function hasDeliverySkipMarker(body, fingerprint) {
|
|
832
|
+
if (!body) return false;
|
|
833
|
+
return body.includes(formatDeliverySkipMarker(fingerprint));
|
|
834
|
+
}
|
|
835
|
+
/** issue 正文(marker 在首位,便于去重判定)。 */
|
|
836
|
+
function buildDeliverySkipIssueBody(input) {
|
|
837
|
+
const repos = input.repositories.length > 0 ? input.repositories.map((repo) => `\`${repo}\``).join("、") : "—";
|
|
838
|
+
const commands = input.failingCommands.length > 0 ? input.failingCommands.map((command) => `\`${command}\``).join("、") : "—";
|
|
839
|
+
return [
|
|
840
|
+
formatDeliverySkipMarker(input.fingerprint),
|
|
841
|
+
"## dependfix 交付被跳过",
|
|
842
|
+
"",
|
|
843
|
+
"修复后安装验证仍为**既有失败**(本次改动前即为红),因此本次修复未被交付:改动已回滚,未推送、未创建 PR。",
|
|
844
|
+
"",
|
|
845
|
+
"| 项 | 值 |",
|
|
846
|
+
"|----|----|",
|
|
847
|
+
`| 运行 | \`${input.runId}\` |`,
|
|
848
|
+
`| 仓库 | ${repos} |`,
|
|
849
|
+
`| 失败命令 | ${commands} |`,
|
|
850
|
+
"",
|
|
851
|
+
"### 需要做什么",
|
|
852
|
+
"",
|
|
853
|
+
"1. 先让目标仓库的安装链恢复(lockfile 与 manifest 一致、registry 可达、依赖脚本完整等),",
|
|
854
|
+
" 使上述失败命令在干净工作区通过。",
|
|
855
|
+
"2. 重新运行 dependfix(`fix-and-pr`)——同一失败指纹不会重复开 issue。",
|
|
856
|
+
"",
|
|
857
|
+
"> 本 issue 由 dependfix 自动创建,按「仓库集 + 失败命令集」指纹去重;关闭后不会自动重开。"
|
|
858
|
+
].join("\n");
|
|
859
|
+
}
|
|
860
|
+
/** issue 标题(仓库集稳定,便于人工按仓库检索)。 */
|
|
861
|
+
function buildDeliverySkipIssueTitle(repositories) {
|
|
862
|
+
return `dependfix 交付跳过:${repositories.length > 0 ? repositories.join(", ") : "target repository"} 安装验证仍有既有失败`;
|
|
863
|
+
}
|
|
497
864
|
/**
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
* 注:此常量仅作为 PAT 路径的默认值;GitHub App 路径接入后走动态 commit author。
|
|
501
|
-
* 现有 PAT 路径仍硬编码 `dependfix[bot]@users.noreply.github.com`,虽非真实 bot 身份(字符串约定),
|
|
502
|
-
* 但保持行为不变以确保 PAT 用户无感升级。已知缺陷由后续阶段修复。
|
|
865
|
+
* 按指纹查找既有交付跳过 issue(含**已关闭**)。
|
|
503
866
|
*
|
|
504
|
-
*
|
|
867
|
+
* 只读取最近 100 条 issue(与尝试索引同口径:失败指纹命中即复用,无需全量翻页);
|
|
868
|
+
* 查询失败抛错由调用方降级(缺省按未命中处理,最多多开一个 issue)。
|
|
505
869
|
*/
|
|
506
|
-
|
|
870
|
+
async function findDeliverySkipIssue(octokit, owner, repo, fingerprint) {
|
|
871
|
+
const { data } = await octokit.rest.issues.listForRepo({
|
|
872
|
+
owner,
|
|
873
|
+
repo,
|
|
874
|
+
state: "all",
|
|
875
|
+
sort: "created",
|
|
876
|
+
direction: "desc",
|
|
877
|
+
per_page: 100
|
|
878
|
+
});
|
|
879
|
+
const hit = data.find((issue) => !("pull_request" in issue) && hasDeliverySkipMarker(issue.body, fingerprint));
|
|
880
|
+
return hit ? {
|
|
881
|
+
number: hit.number,
|
|
882
|
+
htmlUrl: hit.html_url
|
|
883
|
+
} : null;
|
|
884
|
+
}
|
|
885
|
+
/** 创建交付跳过 issue(权限面:`issues: write`)。 */
|
|
886
|
+
async function createDeliverySkipIssue(octokit, owner, repo, params) {
|
|
887
|
+
const { data } = await octokit.rest.issues.create({
|
|
888
|
+
owner,
|
|
889
|
+
repo,
|
|
890
|
+
title: params.title,
|
|
891
|
+
body: params.body,
|
|
892
|
+
...params.labels && params.labels.length > 0 ? { labels: [...params.labels] } : {}
|
|
893
|
+
});
|
|
894
|
+
return {
|
|
895
|
+
number: data.number,
|
|
896
|
+
htmlUrl: data.html_url
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* 草稿交付的 PR 正文说明(仅 `draft` 档渲染;放在正文最前,避免被读成「CI 会绿」)。
|
|
901
|
+
*
|
|
902
|
+
* 返回空数组表示本次运行没有草稿交付(既有 PR 正文逐字不变)。
|
|
903
|
+
*/
|
|
904
|
+
function buildDraftDeliveryNotice(result) {
|
|
905
|
+
const skips = (result.deliverySkips ?? []).filter((record) => record.carrier.mode === "draft");
|
|
906
|
+
if (skips.length === 0) return [];
|
|
907
|
+
const repos = [...new Set(skips.flatMap((record) => record.repositories))];
|
|
908
|
+
const commands = [...new Set(skips.flatMap((record) => record.failingCommands))];
|
|
909
|
+
return [
|
|
910
|
+
"> [!WARNING]",
|
|
911
|
+
"> **草稿交付**:修复后安装验证仍为既有失败(本次改动前即为红),本 PR 以草稿创建。",
|
|
912
|
+
`> 仓库:${repos.length > 0 ? repos.map((repo) => `\`${escapeTableCell(repo)}\``).join("、") : "—"}`,
|
|
913
|
+
`> 失败命令:${commands.length > 0 ? commands.map((command) => `\`${escapeTableCell(command)}\``).join("、") : "—"}`,
|
|
914
|
+
"> CI 会以同一既有失败为红;请先修复目标仓库的安装链,再把本 PR 转正。",
|
|
915
|
+
""
|
|
916
|
+
];
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// src/github/git-config.ts
|
|
920
|
+
/**
|
|
921
|
+
* PAT 路径默认 commit author(保持现有 PAT 路径用户行为零变化)。
|
|
922
|
+
*
|
|
923
|
+
* 注:此常量仅作为 PAT 路径的默认值;GitHub App 路径接入后走动态 commit author。
|
|
924
|
+
* 现有 PAT 路径仍硬编码 `dependfix[bot]@users.noreply.github.com`,虽非真实 bot 身份(字符串约定),
|
|
925
|
+
* 但保持行为不变以确保 PAT 用户无感升级。已知缺陷由后续阶段修复。
|
|
926
|
+
*
|
|
927
|
+
* @see [PAT 无感升级评估 §5.1 兼容性](../../../../docs/design/governance/c22-pat-backward-compat.md)
|
|
928
|
+
*/
|
|
929
|
+
const PAT_DEFAULT_COMMIT_AUTHOR = {
|
|
507
930
|
name: "dependfix[bot]",
|
|
508
931
|
email: "dependfix[bot]@users.noreply.github.com"
|
|
509
932
|
};
|
|
933
|
+
/** 分支是否已存在(`git rev-parse --verify`;非 0 退出即视为不存在)。 */
|
|
934
|
+
async function branchExists(branchName, workDir) {
|
|
935
|
+
const result = await runGit([
|
|
936
|
+
"rev-parse",
|
|
937
|
+
"--verify",
|
|
938
|
+
branchName
|
|
939
|
+
], { cwd: workDir });
|
|
940
|
+
return result.started && result.code === 0;
|
|
941
|
+
}
|
|
942
|
+
/** 本地 git 身份:仅在 local config 缺失时写入(不覆盖用户既有配置)。 */
|
|
943
|
+
async function ensureGitConfig(workDir, author) {
|
|
944
|
+
const effectiveAuthor = author ?? PAT_DEFAULT_COMMIT_AUTHOR;
|
|
945
|
+
const hasName = await gitConfigExists("user.name", workDir);
|
|
946
|
+
const hasEmail = await gitConfigExists("user.email", workDir);
|
|
947
|
+
if (!hasName) await runGitOrThrow([
|
|
948
|
+
"config",
|
|
949
|
+
"user.name",
|
|
950
|
+
effectiveAuthor.name
|
|
951
|
+
], {
|
|
952
|
+
cwd: workDir,
|
|
953
|
+
what: "set git user.name"
|
|
954
|
+
});
|
|
955
|
+
if (!hasEmail) await runGitOrThrow([
|
|
956
|
+
"config",
|
|
957
|
+
"user.email",
|
|
958
|
+
effectiveAuthor.email
|
|
959
|
+
], {
|
|
960
|
+
cwd: workDir,
|
|
961
|
+
what: "set git user.email"
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
async function gitConfigExists(key, workDir) {
|
|
965
|
+
const result = await runGit([
|
|
966
|
+
"config",
|
|
967
|
+
"--local",
|
|
968
|
+
"--get",
|
|
969
|
+
key
|
|
970
|
+
], { cwd: workDir });
|
|
971
|
+
return result.started && result.code === 0;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// src/github/pr-creator.ts
|
|
510
975
|
/** 自动修复分支统一前缀(分支名 = 前缀 + 内容指纹 8 位) */
|
|
511
976
|
const DEPENDFIX_BRANCH_PREFIX = "dependfix/auto-fix-";
|
|
512
977
|
/**
|
|
@@ -573,24 +1038,24 @@ function computeFixAndPrPlan(existingPRs, fingerprint) {
|
|
|
573
1038
|
*
|
|
574
1039
|
* @returns 分支名和是否为新创建
|
|
575
1040
|
*/
|
|
576
|
-
function createFixBranch(branchName, workDir) {
|
|
577
|
-
if (branchExists(branchName, workDir)) {
|
|
578
|
-
|
|
1041
|
+
async function createFixBranch(branchName, workDir) {
|
|
1042
|
+
if (await branchExists(branchName, workDir)) {
|
|
1043
|
+
await runGitOrThrow(["checkout", branchName], {
|
|
579
1044
|
cwd: workDir,
|
|
580
|
-
|
|
1045
|
+
what: `checkout fix branch ${branchName}`
|
|
581
1046
|
});
|
|
582
1047
|
return {
|
|
583
1048
|
branchName,
|
|
584
1049
|
created: false
|
|
585
1050
|
};
|
|
586
1051
|
}
|
|
587
|
-
|
|
1052
|
+
await runGitOrThrow([
|
|
588
1053
|
"checkout",
|
|
589
1054
|
"-b",
|
|
590
1055
|
branchName
|
|
591
1056
|
], {
|
|
592
1057
|
cwd: workDir,
|
|
593
|
-
|
|
1058
|
+
what: `create fix branch ${branchName}`
|
|
594
1059
|
});
|
|
595
1060
|
return {
|
|
596
1061
|
branchName,
|
|
@@ -609,10 +1074,10 @@ function createFixBranch(branchName, workDir) {
|
|
|
609
1074
|
* @throws {CommitScopeError} 提交集合含未声明改动 / 非法声明路径 / 为空
|
|
610
1075
|
* @see [M48.4 提交面白名单 §7](../../../../docs/design/governance/pr-delivery-closure.md#7-m484-提交面白名单与-yaml-定点编辑)
|
|
611
1076
|
*/
|
|
612
|
-
function stageAndCommit(message, workDir, author, options = {}) {
|
|
613
|
-
ensureGitConfig(workDir, author);
|
|
614
|
-
const staging = stageScopedChanges(workDir, options.declaredPaths ?? []);
|
|
615
|
-
commitStaged(message, workDir, author ?? PAT_DEFAULT_COMMIT_AUTHOR);
|
|
1077
|
+
async function stageAndCommit(message, workDir, author, options = {}) {
|
|
1078
|
+
await ensureGitConfig(workDir, author);
|
|
1079
|
+
const staging = await stageScopedChanges(workDir, options.declaredPaths ?? []);
|
|
1080
|
+
await commitStaged(message, workDir, author ?? PAT_DEFAULT_COMMIT_AUTHOR);
|
|
616
1081
|
return staging;
|
|
617
1082
|
}
|
|
618
1083
|
/**
|
|
@@ -624,8 +1089,8 @@ function stageAndCommit(message, workDir, author, options = {}) {
|
|
|
624
1089
|
* - **hooks**:`push --no-verify` 跳过被修复仓库的 `pre-push`(隔离环境无 `npx` / 目标仓库
|
|
625
1090
|
* `node_modules` 不完整时该 hook 会以非 0 退出,把交付链打断为 `push_failed`)
|
|
626
1091
|
*/
|
|
627
|
-
function pushBranch(branchName, workDir) {
|
|
628
|
-
|
|
1092
|
+
async function pushBranch(branchName, workDir) {
|
|
1093
|
+
await runGitOrThrow([
|
|
629
1094
|
...GIT_PUSH_SIGNING_ISOLATION_ARGS,
|
|
630
1095
|
"push",
|
|
631
1096
|
...GIT_PUSH_HOOKS_ISOLATION_ARGS,
|
|
@@ -633,7 +1098,7 @@ function pushBranch(branchName, workDir) {
|
|
|
633
1098
|
branchName
|
|
634
1099
|
], {
|
|
635
1100
|
cwd: workDir,
|
|
636
|
-
|
|
1101
|
+
what: `push branch ${branchName}`
|
|
637
1102
|
});
|
|
638
1103
|
}
|
|
639
1104
|
/**
|
|
@@ -700,17 +1165,18 @@ async function addLabelToPullRequest(octokit, owner, repo, pullNumber, labels) {
|
|
|
700
1165
|
});
|
|
701
1166
|
}
|
|
702
1167
|
/**
|
|
703
|
-
* 通过 GitHub API 创建 Pull Request
|
|
1168
|
+
* 通过 GitHub API 创建 Pull Request(`draft: true` 创建草稿 PR:基线红的交付降级档)。
|
|
704
1169
|
*/
|
|
705
1170
|
async function createPullRequest(params) {
|
|
706
|
-
const { octokit, owner, repo, headBranch, baseBranch, title, body } = params;
|
|
1171
|
+
const { octokit, owner, repo, headBranch, baseBranch, title, body, draft } = params;
|
|
707
1172
|
const { data } = await octokit.rest.pulls.create({
|
|
708
1173
|
owner,
|
|
709
1174
|
repo,
|
|
710
1175
|
title,
|
|
711
1176
|
body,
|
|
712
1177
|
head: headBranch,
|
|
713
|
-
base: baseBranch
|
|
1178
|
+
base: baseBranch,
|
|
1179
|
+
...draft ? { draft: true } : {}
|
|
714
1180
|
});
|
|
715
1181
|
return {
|
|
716
1182
|
number: data.number,
|
|
@@ -833,7 +1299,9 @@ function generatePRBody(result, supersededNumbers, attemptMarkers) {
|
|
|
833
1299
|
const truncatedRows = truncatedBreakdown(summary);
|
|
834
1300
|
const lines = [];
|
|
835
1301
|
if (attemptMarkers && attemptMarkers.length > 0) lines.push(...attemptMarkers.map((marker) => formatAttemptMarker(marker)), "");
|
|
836
|
-
lines.push("## 🔒 Dependfix Auto Fix", "", `**Run ID**: \`${result.runId}\``, `**Mode**: ${result.config.mode}`, `**Time**: ${result.startedAt}`, ""
|
|
1302
|
+
lines.push("## 🔒 Dependfix Auto Fix", "", `**Run ID**: \`${result.runId}\``, `**Mode**: ${result.config.mode}`, `**Time**: ${result.startedAt}`, "");
|
|
1303
|
+
lines.push(...buildDraftDeliveryNotice(result));
|
|
1304
|
+
lines.push("### 📊 Summary", "", "| Metric | Value |", "|--------|-------|", `| Repositories scanned | ${summary.repositoriesScanned} |`, `| Alerts found | ${summary.alertsFound} |`, `| Alerts fixable | ${summary.alertsFixable} |`, `| Dependencies upgraded | ${summary.alertsFixed} |`, `| Upgrades failed | ${summary.alertsFailed} |`, `| Skipped | ${summary.alertsSkipped} |`, `| Converged (already >= target) | ${summary.alertsConverged} |`, `| Truncated (max alerts/repo) | ${summary.alertsTruncated} |`, `| Unfixed alerts | ${unfixed} |`, `| Lockfile repairs | ${summary.lockfileRepairs} |`, `| Verifications passed | ${summary.verificationsPassed} |`, `| Verifications failed | ${summary.verificationsFailed} |`, "");
|
|
837
1305
|
if (unfixed > 0 || skippedRows.length > 0 || truncatedRows.length > 0) {
|
|
838
1306
|
const skippedTotal = skippedRows.reduce((sum, row) => sum + row.count, 0);
|
|
839
1307
|
const failedResidual = Math.max(0, unfixed - skippedTotal);
|
|
@@ -1075,61 +1543,8 @@ function truncatePRBody(lines) {
|
|
|
1075
1543
|
}
|
|
1076
1544
|
return `${kept.join("\n")}${truncationNote}`;
|
|
1077
1545
|
}
|
|
1078
|
-
function branchExists(branchName, workDir) {
|
|
1079
|
-
try {
|
|
1080
|
-
execFileSync("git", [
|
|
1081
|
-
"rev-parse",
|
|
1082
|
-
"--verify",
|
|
1083
|
-
branchName
|
|
1084
|
-
], {
|
|
1085
|
-
cwd: workDir,
|
|
1086
|
-
stdio: "pipe"
|
|
1087
|
-
});
|
|
1088
|
-
return true;
|
|
1089
|
-
} catch {
|
|
1090
|
-
return false;
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
function ensureGitConfig(workDir, author) {
|
|
1094
|
-
const effectiveAuthor = author ?? PAT_DEFAULT_COMMIT_AUTHOR;
|
|
1095
|
-
const hasName = gitConfigExists("user.name", workDir);
|
|
1096
|
-
const hasEmail = gitConfigExists("user.email", workDir);
|
|
1097
|
-
if (!hasName) execFileSync("git", [
|
|
1098
|
-
"config",
|
|
1099
|
-
"user.name",
|
|
1100
|
-
effectiveAuthor.name
|
|
1101
|
-
], {
|
|
1102
|
-
cwd: workDir,
|
|
1103
|
-
stdio: "pipe"
|
|
1104
|
-
});
|
|
1105
|
-
if (!hasEmail) execFileSync("git", [
|
|
1106
|
-
"config",
|
|
1107
|
-
"user.email",
|
|
1108
|
-
effectiveAuthor.email
|
|
1109
|
-
], {
|
|
1110
|
-
cwd: workDir,
|
|
1111
|
-
stdio: "pipe"
|
|
1112
|
-
});
|
|
1113
|
-
}
|
|
1114
|
-
function gitConfigExists(key, workDir) {
|
|
1115
|
-
try {
|
|
1116
|
-
execFileSync("git", [
|
|
1117
|
-
"config",
|
|
1118
|
-
"--local",
|
|
1119
|
-
"--get",
|
|
1120
|
-
key
|
|
1121
|
-
], {
|
|
1122
|
-
cwd: workDir,
|
|
1123
|
-
stdio: "pipe"
|
|
1124
|
-
});
|
|
1125
|
-
return true;
|
|
1126
|
-
} catch {
|
|
1127
|
-
return false;
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
1546
|
|
|
1131
|
-
|
|
1132
|
-
//#region src/github/attempt-history.ts
|
|
1547
|
+
// src/github/attempt-history.ts
|
|
1133
1548
|
/** 默认查询条数(设计稿 §6.2:最近 50 条关闭 PR)。 */
|
|
1134
1549
|
const CLOSED_ATTEMPT_QUERY_LIMIT = 50;
|
|
1135
1550
|
/**
|
|
@@ -1182,8 +1597,7 @@ var ClosedAttemptIndex = class {
|
|
|
1182
1597
|
}
|
|
1183
1598
|
};
|
|
1184
1599
|
|
|
1185
|
-
|
|
1186
|
-
//#region src/verification/validate-commands.ts
|
|
1600
|
+
// src/verification/validate-commands.ts
|
|
1187
1601
|
/** `pnpm <script>` 形态命令匹配(脚本引用) */
|
|
1188
1602
|
const PNPM_SCRIPT_RE = /^pnpm\s+([a-zA-Z][a-zA-Z0-9:_-]*)$/;
|
|
1189
1603
|
/**
|
|
@@ -1236,8 +1650,7 @@ function validateVerifyCommands(commands, workDir) {
|
|
|
1236
1650
|
};
|
|
1237
1651
|
}
|
|
1238
1652
|
|
|
1239
|
-
|
|
1240
|
-
//#region src/github/ci-alignment.ts
|
|
1653
|
+
// src/github/ci-alignment.ts
|
|
1241
1654
|
/** workflow 目录(相对仓库根)。 */
|
|
1242
1655
|
const WORKFLOW_DIR = ".github/workflows";
|
|
1243
1656
|
/** 单次解析的文件数上限(防超大仓库 / 恶意仓库拖慢运行)。 */
|
|
@@ -1809,8 +2222,7 @@ async function buildCiAlignmentRecord(input) {
|
|
|
1809
2222
|
};
|
|
1810
2223
|
}
|
|
1811
2224
|
|
|
1812
|
-
|
|
1813
|
-
//#region src/github/errors.ts
|
|
2225
|
+
// src/github/errors.ts
|
|
1814
2226
|
/**
|
|
1815
2227
|
* 将 Octokit 请求错误映射为 `AppError`。
|
|
1816
2228
|
*
|
|
@@ -1876,8 +2288,7 @@ function collectErrorDetails(error) {
|
|
|
1876
2288
|
return details;
|
|
1877
2289
|
}
|
|
1878
2290
|
|
|
1879
|
-
|
|
1880
|
-
//#region src/github/code-quality-fetcher.ts
|
|
2291
|
+
// src/github/code-quality-fetcher.ts
|
|
1881
2292
|
/** cursor 分页最大页数保护(恶意 Link header 重复同 cursor 或 API 异常时终止循环) */
|
|
1882
2293
|
const MAX_CURSOR_PAGES = 1e3;
|
|
1883
2294
|
/**
|
|
@@ -2012,8 +2423,7 @@ function parseNextCursor(linkHeader) {
|
|
|
2012
2423
|
}
|
|
2013
2424
|
}
|
|
2014
2425
|
|
|
2015
|
-
|
|
2016
|
-
//#region src/code-scanning/rule-config.ts
|
|
2426
|
+
// src/code-scanning/rule-config.ts
|
|
2017
2427
|
/** 默认规则配置(编译后;与既有常量表等价,单点声明保持向后兼容) */
|
|
2018
2428
|
const DEFAULT_RULES_CONFIG = {
|
|
2019
2429
|
autoFixable: /* @__PURE__ */ new Set(["eol-last"]),
|
|
@@ -2136,8 +2546,7 @@ function resetActiveRulesConfig() {
|
|
|
2136
2546
|
activeConfig = DEFAULT_RULES_CONFIG;
|
|
2137
2547
|
}
|
|
2138
2548
|
|
|
2139
|
-
|
|
2140
|
-
//#region src/code-scanning/rule-classifier.ts
|
|
2549
|
+
// src/code-scanning/rule-classifier.ts
|
|
2141
2550
|
/**
|
|
2142
2551
|
* A 类自动修复白名单默认集合(默认值常量;测试 disjointness 不变量用)。
|
|
2143
2552
|
*
|
|
@@ -2199,8 +2608,7 @@ function suggestionFor(ruleId) {
|
|
|
2199
2608
|
return getActiveRulesConfig().suggested.get(id) ?? "人工审查该 Code Scanning 告警:结合业务语义评估修复方案(参考 CodeQL 规则文档)";
|
|
2200
2609
|
}
|
|
2201
2610
|
|
|
2202
|
-
|
|
2203
|
-
//#region src/github/code-scanning-fetcher.ts
|
|
2611
|
+
// src/github/code-scanning-fetcher.ts
|
|
2204
2612
|
/**
|
|
2205
2613
|
* 拉取指定仓库的 Code Scanning 告警并映射为标准化模型。
|
|
2206
2614
|
*
|
|
@@ -2283,8 +2691,7 @@ function mapSeverity(alert) {
|
|
|
2283
2691
|
return "unknown";
|
|
2284
2692
|
}
|
|
2285
2693
|
|
|
2286
|
-
|
|
2287
|
-
//#region src/github/dependabot-fetcher.ts
|
|
2694
|
+
// src/github/dependabot-fetcher.ts
|
|
2288
2695
|
/**
|
|
2289
2696
|
* 拉取指定仓库的 Dependabot 告警并映射为标准化模型。
|
|
2290
2697
|
*
|
|
@@ -2379,8 +2786,7 @@ function normalizeDependencyRelationship(relationship) {
|
|
|
2379
2786
|
if (relationship === "indirect") return "transitive";
|
|
2380
2787
|
}
|
|
2381
2788
|
|
|
2382
|
-
|
|
2383
|
-
//#region src/github/pr-check-watch.ts
|
|
2789
|
+
// src/github/pr-check-watch.ts
|
|
2384
2790
|
/** 首次轮询间隔。 */
|
|
2385
2791
|
const PR_CHECK_POLL_INITIAL_MS = 15e3;
|
|
2386
2792
|
/** 轮询间隔退避上限。 */
|
|
@@ -2483,15 +2889,20 @@ async function collectObservedChecks(octokit, owner, repo, ref) {
|
|
|
2483
2889
|
owner,
|
|
2484
2890
|
repo,
|
|
2485
2891
|
ref,
|
|
2486
|
-
per_page: 100
|
|
2892
|
+
per_page: 100,
|
|
2893
|
+
filter: "all"
|
|
2487
2894
|
});
|
|
2488
2895
|
for (const run of checkRuns) {
|
|
2489
2896
|
if (run?.id === void 0 || run?.id === null) continue;
|
|
2490
2897
|
checks.push({
|
|
2898
|
+
kind: "check",
|
|
2899
|
+
provider: resolveCheckProvider(run.app?.slug),
|
|
2491
2900
|
name: sanitizeCheckName(run.name),
|
|
2492
2901
|
conclusion: normalizeCheckRunConclusion(run.status, run.conclusion),
|
|
2493
2902
|
url: sanitizeCheckUrl(run.details_url ?? run.html_url),
|
|
2494
|
-
detail: truncateDetail([run.output?.title, run.output?.summary].filter(Boolean).join(" — "))
|
|
2903
|
+
detail: truncateDetail([run.output?.title, run.output?.summary].filter(Boolean).join(" — ")),
|
|
2904
|
+
id: String(run.id),
|
|
2905
|
+
roundAt: run.started_at ?? run.completed_at ?? null
|
|
2495
2906
|
});
|
|
2496
2907
|
}
|
|
2497
2908
|
} catch (error) {
|
|
@@ -2507,10 +2918,14 @@ async function collectObservedChecks(octokit, owner, repo, ref) {
|
|
|
2507
2918
|
for (const status of statuses) {
|
|
2508
2919
|
if (status?.id === void 0 || status?.id === null) continue;
|
|
2509
2920
|
checks.push({
|
|
2921
|
+
kind: "status",
|
|
2922
|
+
provider: resolveStatusProvider(status.context),
|
|
2510
2923
|
name: sanitizeCheckName(status.context),
|
|
2511
2924
|
conclusion: normalizeCommitStatusConclusion(status.state),
|
|
2512
2925
|
url: sanitizeCheckUrl(status.target_url),
|
|
2513
|
-
detail: truncateDetail(status.description)
|
|
2926
|
+
detail: truncateDetail(status.description),
|
|
2927
|
+
id: String(status.id),
|
|
2928
|
+
roundAt: status.updated_at ?? status.created_at ?? null
|
|
2514
2929
|
});
|
|
2515
2930
|
}
|
|
2516
2931
|
} catch (error) {
|
|
@@ -2581,14 +2996,36 @@ async function resolveParentSha(params) {
|
|
|
2581
2996
|
}
|
|
2582
2997
|
}
|
|
2583
2998
|
/**
|
|
2999
|
+
* 同名多轮收敛:同一「组键」只保留**最新一轮**(与平台 `dedupeSignalsByLatestRound` 同源,
|
|
3000
|
+
* 组键与排序均由 `@dependfix/core` 的 `checkRoundKey` / `dedupeByLatestRound` 提供)。
|
|
3001
|
+
*
|
|
3002
|
+
* 触发场景:check run re-run(同一 workflow run 的同名新尝试)、同一 commit status context 的后发状态。
|
|
3003
|
+
* 组键含 `kind` / `provider`:check run 与 commit status 同名、或同名 job 由不同 App 上报时**不合并**;
|
|
3004
|
+
* check 还要求能证明同一条 workflow run(`details_url` 里的 run id),否则各自成组——
|
|
3005
|
+
* 「push 触发」与「pull_request 触发」是同名不同 run,合并会掩蔽另一方的失败。
|
|
3006
|
+
* 口径见 docs/design/governance/pr-build-deploy-signals.md §3.10。
|
|
3007
|
+
*/
|
|
3008
|
+
function dedupeObservedChecksByLatestRound(checks) {
|
|
3009
|
+
return dedupeByLatestRound(checks, (check, index) => checkRoundKey({
|
|
3010
|
+
kind: check.kind,
|
|
3011
|
+
provider: check.provider,
|
|
3012
|
+
name: check.name,
|
|
3013
|
+
url: check.url,
|
|
3014
|
+
sourceId: check.id
|
|
3015
|
+
}, String(index)));
|
|
3016
|
+
}
|
|
3017
|
+
/**
|
|
2584
3018
|
* 判定终态;未到终态返回 `undefined`(继续轮询)。
|
|
2585
3019
|
*
|
|
2586
3020
|
* 终态条件:已观测到至少一条 check,且全部 check 已完成(无 `pending`)。
|
|
3021
|
+
* `completed + conclusion=null` 归一为 `unknown` 结论(`isFailedConclusion` 判失败,fail-closed),
|
|
3022
|
+
* 与平台侧 `resolveCheckRunState` 同口径(docs/design/governance/pr-build-deploy-signals.md §3.10)。
|
|
2587
3023
|
* - 存在失败 → `failed`(逐条归因后聚合;混合归因 → `unknown`)
|
|
2588
3024
|
* - 无失败且有成功 → `passed`
|
|
2589
3025
|
* - 无失败且无成功(全是 cancelled / skipped / neutral)→ `cancelled`
|
|
2590
3026
|
*/
|
|
2591
|
-
async function evaluateCheckTerminal(params,
|
|
3027
|
+
async function evaluateCheckTerminal(params, observedChecks) {
|
|
3028
|
+
const observed = dedupeObservedChecksByLatestRound(observedChecks);
|
|
2592
3029
|
if (observed.length === 0) return;
|
|
2593
3030
|
if (observed.some((check) => check.conclusion === "pending")) return;
|
|
2594
3031
|
const failures = observed.filter((check) => isFailedConclusion(check.conclusion));
|
|
@@ -2811,8 +3248,7 @@ async function watchPullRequestChecks(params) {
|
|
|
2811
3248
|
}
|
|
2812
3249
|
}
|
|
2813
3250
|
|
|
2814
|
-
|
|
2815
|
-
//#region src/github/repo-policy.ts
|
|
3251
|
+
// src/github/repo-policy.ts
|
|
2816
3252
|
/** 单条 glob 模式长度上限(加固:防止超长模式引入匹配开销;受信配置 + 短输入) */
|
|
2817
3253
|
const MAX_GLOB_PATTERN_LENGTH = 200;
|
|
2818
3254
|
/**
|
|
@@ -2911,8 +3347,7 @@ function filterExplicitRepositories(policy, repositories) {
|
|
|
2911
3347
|
return repositories.filter((repo) => !matchesRepoExclude(policy, repo));
|
|
2912
3348
|
}
|
|
2913
3349
|
|
|
2914
|
-
|
|
2915
|
-
//#region src/github/repo-selector.ts
|
|
3350
|
+
// src/github/repo-selector.ts
|
|
2916
3351
|
/**
|
|
2917
3352
|
* 从文件读取仓库列表(每行一个 `owner/repo`)。
|
|
2918
3353
|
* 空行和 `#` 开头的注释行会被跳过。
|
|
@@ -2944,8 +3379,7 @@ function resolveRepoList(cliRepos, reposFilePath) {
|
|
|
2944
3379
|
return deduped;
|
|
2945
3380
|
}
|
|
2946
3381
|
|
|
2947
|
-
|
|
2948
|
-
//#region src/github/repository-discovery.ts
|
|
3382
|
+
// src/github/repository-discovery.ts
|
|
2949
3383
|
const DEPENDABOT_CONFIG_PATH = ".github/dependabot.yml";
|
|
2950
3384
|
/**
|
|
2951
3385
|
* 按 owner / org 自动发现仓库,产出稳定处理清单。
|
|
@@ -3079,8 +3513,7 @@ async function probeDependabotConfig(client, owner, repo) {
|
|
|
3079
3513
|
}
|
|
3080
3514
|
}
|
|
3081
3515
|
|
|
3082
|
-
|
|
3083
|
-
//#region src/github/token-scope.ts
|
|
3516
|
+
// src/github/token-scope.ts
|
|
3084
3517
|
const CLASSIC_REPO_SCOPE_WARNING = {
|
|
3085
3518
|
code: "CLASSIC_REPO_SCOPE",
|
|
3086
3519
|
message: "检测到 classic PAT 且包含 repo(全量仓库)权限——该 token 一旦被恶意依赖脚本窃取,即可接管所有可见仓库。建议改用 fine-grained PAT(最小权限:contents/pull-requests 写 + security-events 读,仅目标仓库),详见 quick-start 安全注意事项。"
|
|
@@ -3152,8 +3585,7 @@ async function checkTokenPermissions(client, options = {}) {
|
|
|
3152
3585
|
}
|
|
3153
3586
|
}
|
|
3154
3587
|
|
|
3155
|
-
|
|
3156
|
-
//#region src/code-scanning/templates.ts
|
|
3588
|
+
// src/code-scanning/templates.ts
|
|
3157
3589
|
const TEMPLATES = new Map([{
|
|
3158
3590
|
ruleId: "eol-last",
|
|
3159
3591
|
apply(_filePath, content) {
|
|
@@ -3176,8 +3608,7 @@ function getCodeScanningFixTemplate(ruleId) {
|
|
|
3176
3608
|
return TEMPLATES.get(ruleId);
|
|
3177
3609
|
}
|
|
3178
3610
|
|
|
3179
|
-
|
|
3180
|
-
//#region src/fixers/install-timeouts.ts
|
|
3611
|
+
// src/fixers/install-timeouts.ts
|
|
3181
3612
|
/** 安装类命令默认超时:5 分钟。 */
|
|
3182
3613
|
const DEFAULT_INSTALL_TIMEOUT_MS = 3e5;
|
|
3183
3614
|
/** 安装类命令超时下限(低于此值视为配置错误 → 回退默认)。 */
|
|
@@ -3201,8 +3632,7 @@ function resolveInstallTimeoutMs(env = process.env, fallback = DEFAULT_INSTALL_T
|
|
|
3201
3632
|
return Number.isInteger(value) && value >= 1e4 && value <= 18e5 ? value : fallback;
|
|
3202
3633
|
}
|
|
3203
3634
|
|
|
3204
|
-
|
|
3205
|
-
//#region src/runners/output-sanitizer.ts
|
|
3635
|
+
// src/runners/output-sanitizer.ts
|
|
3206
3636
|
/** 敏感输出匹配规则(顺序固定,后者可覆盖前者的部分结果)。 */
|
|
3207
3637
|
const SECRET_PATTERNS$1 = [
|
|
3208
3638
|
[/^(GITHUB_TOKEN|NPM_TOKEN|NODE_AUTH_TOKEN|DOCKER_PASSWORD)=.+/gim, "$1=***"],
|
|
@@ -3217,8 +3647,7 @@ function sanitizeOutput(output) {
|
|
|
3217
3647
|
return result;
|
|
3218
3648
|
}
|
|
3219
3649
|
|
|
3220
|
-
|
|
3221
|
-
//#region src/fixers/process-signals.ts
|
|
3650
|
+
// src/fixers/process-signals.ts
|
|
3222
3651
|
/**
|
|
3223
3652
|
* 进程级失败信号抽取(`execSync` / `execFileSync` 抛出错误的统一读取口径)。
|
|
3224
3653
|
*
|
|
@@ -3270,286 +3699,67 @@ function extractProcessSignals(err) {
|
|
|
3270
3699
|
return {
|
|
3271
3700
|
exitCode: typeof err.status === "number" ? err.status : null,
|
|
3272
3701
|
signal: typeof err.signal === "string" ? err.signal : null,
|
|
3273
|
-
timedOut: err.code === "ETIMEDOUT",
|
|
3274
|
-
errno: typeof err.code === "string" && err.code !== "ETIMEDOUT" ? err.code : null
|
|
3275
|
-
};
|
|
3276
|
-
}
|
|
3277
|
-
/**
|
|
3278
|
-
* 安装失败是否属于环境性问题(**单一判据**:lockfile 诊断链与依赖升级链共用)。
|
|
3279
|
-
*
|
|
3280
|
-
* 判定顺序:
|
|
3281
|
-
* 1. 运行级取消(`aborted`)→ false:中止是独立类别(`ABORTED`),不得伪装成
|
|
3282
|
-
* 「不归因目标仓库」的环境性失败。
|
|
3283
|
-
* 2. 信号终止 / 超时 → true:进程可能已吐出部分输出,但被杀死 / 超时本身说明执行环境异常,
|
|
3284
|
-
* 不足以把失败归因目标仓库。
|
|
3285
|
-
* 3. 有 stderr 诊断 → false:`ERR_PNPM_*` / 解析失败等可读失败归因目标仓库。
|
|
3286
|
-
* 4. stderr 为空 → true(静默非零退出)。
|
|
3287
|
-
*
|
|
3288
|
-
* 归因只看 stderr,**不看 stdout**:pnpm 11 把安装脚本错误(`gyp ERR!` / `prebuild-install`)
|
|
3289
|
-
* 写到 stdout,那类原生构建失败仍按环境性处理(stdout 作为证据保留,口径见
|
|
3290
|
-
* docs/design/modules/pnpm-lockfile-fixer.md §2.4)。
|
|
3291
|
-
*/
|
|
3292
|
-
function isEnvironmentalInstallError(err) {
|
|
3293
|
-
if (err.aborted === true) return false;
|
|
3294
|
-
const signals = extractProcessSignals(err);
|
|
3295
|
-
if (signals.signal !== null || signals.timedOut) return true;
|
|
3296
|
-
return getRawStderrText(err).trim().length === 0;
|
|
3297
|
-
}
|
|
3298
|
-
/** 渲染进程级信号的简短描述(用于失败详情);无可用信号时返回空串。 */
|
|
3299
|
-
function describeProcessSignals(signals) {
|
|
3300
|
-
const parts = [];
|
|
3301
|
-
if (signals.signal !== null) parts.push(`signal ${signals.signal}`);
|
|
3302
|
-
else if (signals.exitCode !== null) parts.push(`exit code ${signals.exitCode}`);
|
|
3303
|
-
else if (signals.errno !== null) parts.push(`spawn error ${signals.errno}`);
|
|
3304
|
-
if (signals.timedOut) parts.push("timed out");
|
|
3305
|
-
return parts.join(", ");
|
|
3306
|
-
}
|
|
3307
|
-
/**
|
|
3308
|
-
* 把受管命令(异步 `spawn`)的结果适配为既有 `execSync` / `execFileSync` 形态的错误对象。
|
|
3309
|
-
*
|
|
3310
|
-
* 目的:安装类调用从同步 `execFileSync` 迁到 `runTrackedCommand` 后,
|
|
3311
|
-
* `extractProcessSignals` / `getRawStderrText` / `getRawStdoutText` / `describeInstallError`
|
|
3312
|
-
* 的读取口径**零漂移**——`status` / `signal` / `code` / `stderr` / `stdout` 字段与 Node 抛出的
|
|
3313
|
-
* 形态一致(`status` 仅在有退出码时设置,`code` 在超时时为 `ETIMEDOUT`、spawn 失败时为 `errno`)。
|
|
3314
|
-
*/
|
|
3315
|
-
function toExecLikeError(command, outcome) {
|
|
3316
|
-
let message = `Command failed: ${command}`;
|
|
3317
|
-
if (outcome.aborted) message = `run aborted — command terminated by cancellation: ${command}`;
|
|
3318
|
-
else if (outcome.timedOut) message = `spawnSync ${command} ETIMEDOUT`;
|
|
3319
|
-
else if (outcome.errno) message = `spawn ${command} ${outcome.errno}`;
|
|
3320
|
-
if (outcome.terminationUnconfirmed) message += " (termination unconfirmed)";
|
|
3321
|
-
const err = new Error(message);
|
|
3322
|
-
if (outcome.aborted) err.aborted = true;
|
|
3323
|
-
if (outcome.terminationUnconfirmed) err.terminationUnconfirmed = true;
|
|
3324
|
-
if (outcome.stderr) err.stderr = outcome.stderr;
|
|
3325
|
-
if (outcome.stdout) err.stdout = outcome.stdout;
|
|
3326
|
-
if (outcome.exitCode !== null) err.status = outcome.exitCode;
|
|
3327
|
-
if (outcome.signal !== null) err.signal = outcome.signal;
|
|
3328
|
-
if (outcome.timedOut) err.code = "ETIMEDOUT";
|
|
3329
|
-
else if (outcome.errno !== null) err.code = outcome.errno;
|
|
3330
|
-
return err;
|
|
3331
|
-
}
|
|
3332
|
-
|
|
3333
|
-
//#endregion
|
|
3334
|
-
//#region src/runners/process-control.ts
|
|
3335
|
-
/** 单流捕获上限(与 `execSync` 默认 `maxBuffer` 同量级):超限后继续 drain 但不再累积。 */
|
|
3336
|
-
const MAX_STREAM_CAPTURE_BYTES = 1048576;
|
|
3337
|
-
/**
|
|
3338
|
-
* 子进程退出后等待 stdio 关闭的有界宽限期。
|
|
3339
|
-
*
|
|
3340
|
-
* `close` 事件只有在**所有**持有 stdout / stderr 写端的进程都退出后才会触发;安装脚本
|
|
3341
|
-
* (node-gyp / prebuild-install / postinstall)可能派生出脱离直接子进程生命周期的孙进程,
|
|
3342
|
-
* 即使进程组已被终止,只要孙进程仍持有管道,`close` 就永远不会到来。实测一次 5 分钟
|
|
3343
|
-
* 安装超时因此拖到 12 分钟才落定,直接吃掉单仓 30 分钟执行预算。
|
|
3344
|
-
*
|
|
3345
|
-
* 超时只约束"等待退出",不改变输出语义:宽限期内正常关闭仍走 `close`;超期则主动
|
|
3346
|
-
* destroy 管道并用已捕获的输出落定。
|
|
3347
|
-
*/
|
|
3348
|
-
const STREAM_CLOSE_GRACE_MS = 2e3;
|
|
3349
|
-
/** 运行级 abort 原因(`undefined` = 未请求取消)。 */
|
|
3350
|
-
let abortReason;
|
|
3351
|
-
/** 已登记的活跃子进程 PID(超时 / abort 时按进程树终止)。 */
|
|
3352
|
-
const trackedPids = /* @__PURE__ */ new Set();
|
|
3353
|
-
/**
|
|
3354
|
-
* 复位运行级进程控制状态(每次构造 `DependfixApp` 时调用)。
|
|
3355
|
-
*
|
|
3356
|
-
* 只清状态、**不杀进程**:复位发生在两次运行之间,此时上一次运行的子进程应由其调用方
|
|
3357
|
-
* 正常收尾(`runTrackedCommand` 在 close / error 时已注销)。
|
|
3358
|
-
*/
|
|
3359
|
-
function resetProcessControlState() {
|
|
3360
|
-
abortReason = void 0;
|
|
3361
|
-
trackedPids.clear();
|
|
3362
|
-
}
|
|
3363
|
-
/**
|
|
3364
|
-
* 请求运行级取消(幂等)。
|
|
3365
|
-
*
|
|
3366
|
-
* 首次调用记录原因;**每次调用都终止当前已登记的子进程树**——这样在"平台放弃等待"之后,
|
|
3367
|
-
* 正在跑的 install / 验证命令会被立即杀掉,而不是继续与新仓库争抢资源。
|
|
3368
|
-
*/
|
|
3369
|
-
function requestAbort(reason) {
|
|
3370
|
-
if (abortReason === void 0) abortReason = reason ?? "abort requested";
|
|
3371
|
-
killAllTrackedChildProcesses();
|
|
3372
|
-
}
|
|
3373
|
-
/** 是否已请求取消(调用方在启动新命令 / 新仓库之前短路)。 */
|
|
3374
|
-
function isAbortRequested() {
|
|
3375
|
-
return abortReason !== void 0;
|
|
3376
|
-
}
|
|
3377
|
-
/** 取消原因(未取消时为 `undefined`)。 */
|
|
3378
|
-
function getAbortReason() {
|
|
3379
|
-
return abortReason;
|
|
3380
|
-
}
|
|
3381
|
-
/** 登记活跃子进程 PID(无 PID / 非法 PID 静默忽略——如 spawn 立即失败)。 */
|
|
3382
|
-
function trackChildProcess(pid) {
|
|
3383
|
-
if (typeof pid === "number" && pid > 0) trackedPids.add(pid);
|
|
3384
|
-
}
|
|
3385
|
-
/** 注销活跃子进程 PID(进程已退出)。 */
|
|
3386
|
-
function untrackChildProcess(pid) {
|
|
3387
|
-
if (typeof pid === "number" && pid > 0) trackedPids.delete(pid);
|
|
3388
|
-
}
|
|
3389
|
-
/**
|
|
3390
|
-
* 终止所有已登记的子进程树,返回终止数量。
|
|
3391
|
-
*
|
|
3392
|
-
* 终止后立即清空登记表:再次调用不会重复 kill(PID 可能已被系统复用)。
|
|
3393
|
-
*/
|
|
3394
|
-
function killAllTrackedChildProcesses() {
|
|
3395
|
-
let killed = 0;
|
|
3396
|
-
for (const pid of [...trackedPids]) {
|
|
3397
|
-
killProcessTree(pid);
|
|
3398
|
-
trackedPids.delete(pid);
|
|
3399
|
-
killed++;
|
|
3400
|
-
}
|
|
3401
|
-
return killed;
|
|
3402
|
-
}
|
|
3403
|
-
/**
|
|
3404
|
-
* 终止进程树:POSIX 杀进程组(spawn 时 `detached` 使子进程成为进程组组长),
|
|
3405
|
-
* Windows 用 `taskkill /T /F`。失败时静默退化(调用方另有直接 `kill` 兜底)。
|
|
3406
|
-
*
|
|
3407
|
-
* 单一事实源:verification-runner 的超时终止与本模块的 abort 终止共用此实现。
|
|
3408
|
-
*/
|
|
3409
|
-
function killProcessTree(pid) {
|
|
3410
|
-
if (pid === void 0 || pid <= 0) return;
|
|
3411
|
-
if (process.platform === "win32") try {
|
|
3412
|
-
spawnSync("taskkill", [
|
|
3413
|
-
"/pid",
|
|
3414
|
-
String(pid),
|
|
3415
|
-
"/T",
|
|
3416
|
-
"/F"
|
|
3417
|
-
], { stdio: "ignore" });
|
|
3418
|
-
} catch {}
|
|
3419
|
-
else try {
|
|
3420
|
-
process.kill(-pid, "SIGKILL");
|
|
3421
|
-
} catch {}
|
|
3422
|
-
}
|
|
3423
|
-
/**
|
|
3424
|
-
* 是否经 shell 执行(Windows 需要;见模块头「平台差异」)。
|
|
3425
|
-
*
|
|
3426
|
-
* 抽成纯函数以便用例锁定平台分支:仅 `win32` 返回 true,其余平台 false
|
|
3427
|
-
* (POSIX 保持直接 `spawn`,与改造前行为一致)。
|
|
3428
|
-
*/
|
|
3429
|
-
function resolveShellOption(platform = process.platform) {
|
|
3430
|
-
return platform === "win32";
|
|
3431
|
-
}
|
|
3432
|
-
/**
|
|
3433
|
-
* 异步执行命令并登记子进程(超时 / abort 时按进程树终止)。
|
|
3434
|
-
*
|
|
3435
|
-
* **取消前置守卫**:调用时若已请求取消,直接返回 `started: false`(不 spawn)——这是
|
|
3436
|
-
* "abort 后不再启动新工作"在安装 / 验证类命令上的**单一入口**(各调用方无需各写一遍)。
|
|
3437
|
-
*
|
|
3438
|
-
* 与旧 `execFileSync` 的差异(有意为之):
|
|
3439
|
-
* - **不阻塞事件循环**——引擎与平台同进程运行,同步调用会让平台 HTTP、定时器(含执行超时)
|
|
3440
|
-
* 与队列全部停摆(实测:超时定时器比设定值晚约 9.5 分钟才触发)
|
|
3441
|
-
* - **输出限长累积**:超过 `MAX_STREAM_CAPTURE_BYTES` 后继续 drain(避免子进程写满管道阻塞)
|
|
3442
|
-
* 但不再累积,防大输出把内存吃掉
|
|
3443
|
-
* - 不抛错:退出码 / 信号 / 超时 / errno 全部经返回值暴露,由调用方决定语义
|
|
3444
|
-
*/
|
|
3445
|
-
function runTrackedCommand(params) {
|
|
3446
|
-
if (isAbortRequested()) return Promise.resolve({
|
|
3447
|
-
started: false,
|
|
3448
|
-
exitCode: null,
|
|
3449
|
-
signal: null,
|
|
3450
|
-
timedOut: false,
|
|
3451
|
-
aborted: true,
|
|
3452
|
-
errno: null,
|
|
3453
|
-
stdout: "",
|
|
3454
|
-
stderr: "",
|
|
3455
|
-
durationMs: 0
|
|
3456
|
-
});
|
|
3457
|
-
return new Promise((resolve) => {
|
|
3458
|
-
const startedAt = Date.now();
|
|
3459
|
-
let timedOut = false;
|
|
3460
|
-
const stdoutChunks = [];
|
|
3461
|
-
const stderrChunks = [];
|
|
3462
|
-
let stdoutBytes = 0;
|
|
3463
|
-
let stderrBytes = 0;
|
|
3464
|
-
const cp = spawn(params.binary, params.args, {
|
|
3465
|
-
cwd: params.cwd,
|
|
3466
|
-
stdio: "pipe",
|
|
3467
|
-
shell: params.shell ?? resolveShellOption(),
|
|
3468
|
-
detached: process.platform !== "win32",
|
|
3469
|
-
...params.env ? { env: params.env } : {}
|
|
3470
|
-
});
|
|
3471
|
-
trackChildProcess(cp.pid);
|
|
3472
|
-
let settled = false;
|
|
3473
|
-
let exited = false;
|
|
3474
|
-
let closeGraceTimer;
|
|
3475
|
-
const timer = setTimeout(() => {
|
|
3476
|
-
timedOut = true;
|
|
3477
|
-
killProcessTree(cp.pid);
|
|
3478
|
-
cp.kill("SIGKILL");
|
|
3479
|
-
settleAfterStreamGrace({
|
|
3480
|
-
exitCode: null,
|
|
3481
|
-
signal: "SIGKILL"
|
|
3482
|
-
});
|
|
3483
|
-
}, params.timeoutMs);
|
|
3484
|
-
function finish(result) {
|
|
3485
|
-
if (settled) return;
|
|
3486
|
-
settled = true;
|
|
3487
|
-
clearTimeout(timer);
|
|
3488
|
-
if (closeGraceTimer) clearTimeout(closeGraceTimer);
|
|
3489
|
-
untrackChildProcess(cp.pid);
|
|
3490
|
-
resolve({
|
|
3491
|
-
started: true,
|
|
3492
|
-
exitCode: result.exitCode,
|
|
3493
|
-
signal: result.signal,
|
|
3494
|
-
timedOut,
|
|
3495
|
-
terminationUnconfirmed: timedOut && !exited ? true : void 0,
|
|
3496
|
-
aborted: isAbortRequested(),
|
|
3497
|
-
errno: result.errno ?? null,
|
|
3498
|
-
stdout: stdoutChunks.join(""),
|
|
3499
|
-
stderr: stderrChunks.join(""),
|
|
3500
|
-
durationMs: Date.now() - startedAt
|
|
3501
|
-
});
|
|
3502
|
-
}
|
|
3503
|
-
/**
|
|
3504
|
-
* 直接子进程已退出(或已被主动终止)后,等待 stdio 关闭的**有界**窗口。
|
|
3505
|
-
* `close` 正常到来时由 `finish` 清掉本定时器;孙进程持有管道时由本定时器兜底。
|
|
3506
|
-
*/
|
|
3507
|
-
function settleAfterStreamGrace(result) {
|
|
3508
|
-
if (settled) return;
|
|
3509
|
-
if (closeGraceTimer) clearTimeout(closeGraceTimer);
|
|
3510
|
-
closeGraceTimer = setTimeout(() => {
|
|
3511
|
-
killProcessTree(cp.pid);
|
|
3512
|
-
cp.stdout?.destroy?.();
|
|
3513
|
-
cp.stderr?.destroy?.();
|
|
3514
|
-
finish(result);
|
|
3515
|
-
}, STREAM_CLOSE_GRACE_MS);
|
|
3516
|
-
}
|
|
3517
|
-
const capture = (chunks, bytes, data) => {
|
|
3518
|
-
const next = bytes + data.byteLength;
|
|
3519
|
-
if (bytes < MAX_STREAM_CAPTURE_BYTES) chunks.push(data.toString("utf-8"));
|
|
3520
|
-
return next;
|
|
3521
|
-
};
|
|
3522
|
-
cp.stdout?.on("data", (data) => {
|
|
3523
|
-
stdoutBytes = capture(stdoutChunks, stdoutBytes, data);
|
|
3524
|
-
});
|
|
3525
|
-
cp.stderr?.on("data", (data) => {
|
|
3526
|
-
stderrBytes = capture(stderrChunks, stderrBytes, data);
|
|
3527
|
-
});
|
|
3528
|
-
cp.on("error", (err) => {
|
|
3529
|
-
finish({
|
|
3530
|
-
exitCode: null,
|
|
3531
|
-
signal: null,
|
|
3532
|
-
errno: err.code ?? "UNKNOWN"
|
|
3533
|
-
});
|
|
3534
|
-
});
|
|
3535
|
-
cp.on("exit", (code, signal) => {
|
|
3536
|
-
exited = true;
|
|
3537
|
-
settleAfterStreamGrace({
|
|
3538
|
-
exitCode: code,
|
|
3539
|
-
signal
|
|
3540
|
-
});
|
|
3541
|
-
});
|
|
3542
|
-
cp.on("close", (code, signal) => {
|
|
3543
|
-
finish({
|
|
3544
|
-
exitCode: code,
|
|
3545
|
-
signal
|
|
3546
|
-
});
|
|
3547
|
-
});
|
|
3548
|
-
});
|
|
3702
|
+
timedOut: err.code === "ETIMEDOUT",
|
|
3703
|
+
errno: typeof err.code === "string" && err.code !== "ETIMEDOUT" ? err.code : null
|
|
3704
|
+
};
|
|
3705
|
+
}
|
|
3706
|
+
/**
|
|
3707
|
+
* 安装失败是否属于环境性问题(**单一判据**:lockfile 诊断链与依赖升级链共用)。
|
|
3708
|
+
*
|
|
3709
|
+
* 判定顺序:
|
|
3710
|
+
* 1. 运行级取消(`aborted`)→ false:中止是独立类别(`ABORTED`),不得伪装成
|
|
3711
|
+
* 「不归因目标仓库」的环境性失败。
|
|
3712
|
+
* 2. 信号终止 / 超时 → true:进程可能已吐出部分输出,但被杀死 / 超时本身说明执行环境异常,
|
|
3713
|
+
* 不足以把失败归因目标仓库。
|
|
3714
|
+
* 3. 有 stderr 诊断 → false:`ERR_PNPM_*` / 解析失败等可读失败归因目标仓库。
|
|
3715
|
+
* 4. stderr 为空 → true(静默非零退出)。
|
|
3716
|
+
*
|
|
3717
|
+
* 归因只看 stderr,**不看 stdout**:pnpm 11 把安装脚本错误(`gyp ERR!` / `prebuild-install`)
|
|
3718
|
+
* 写到 stdout,那类原生构建失败仍按环境性处理(stdout 作为证据保留,口径见
|
|
3719
|
+
* docs/design/modules/pnpm-lockfile-fixer.md §2.4)。
|
|
3720
|
+
*/
|
|
3721
|
+
function isEnvironmentalInstallError(err) {
|
|
3722
|
+
if (err.aborted === true) return false;
|
|
3723
|
+
const signals = extractProcessSignals(err);
|
|
3724
|
+
if (signals.signal !== null || signals.timedOut) return true;
|
|
3725
|
+
return getRawStderrText(err).trim().length === 0;
|
|
3726
|
+
}
|
|
3727
|
+
/** 渲染进程级信号的简短描述(用于失败详情);无可用信号时返回空串。 */
|
|
3728
|
+
function describeProcessSignals(signals) {
|
|
3729
|
+
const parts = [];
|
|
3730
|
+
if (signals.signal !== null) parts.push(`signal ${signals.signal}`);
|
|
3731
|
+
else if (signals.exitCode !== null) parts.push(`exit code ${signals.exitCode}`);
|
|
3732
|
+
else if (signals.errno !== null) parts.push(`spawn error ${signals.errno}`);
|
|
3733
|
+
if (signals.timedOut) parts.push("timed out");
|
|
3734
|
+
return parts.join(", ");
|
|
3735
|
+
}
|
|
3736
|
+
/**
|
|
3737
|
+
* 把受管命令(异步 `spawn`)的结果适配为既有 `execSync` / `execFileSync` 形态的错误对象。
|
|
3738
|
+
*
|
|
3739
|
+
* 目的:安装类调用从同步 `execFileSync` 迁到 `runTrackedCommand` 后,
|
|
3740
|
+
* `extractProcessSignals` / `getRawStderrText` / `getRawStdoutText` / `describeInstallError`
|
|
3741
|
+
* 的读取口径**零漂移**——`status` / `signal` / `code` / `stderr` / `stdout` 字段与 Node 抛出的
|
|
3742
|
+
* 形态一致(`status` 仅在有退出码时设置,`code` 在超时时为 `ETIMEDOUT`、spawn 失败时为 `errno`)。
|
|
3743
|
+
*/
|
|
3744
|
+
function toExecLikeError(command, outcome) {
|
|
3745
|
+
let message = `Command failed: ${command}`;
|
|
3746
|
+
if (outcome.aborted) message = `run aborted — command terminated by cancellation: ${command}`;
|
|
3747
|
+
else if (outcome.timedOut) message = `spawnSync ${command} ETIMEDOUT`;
|
|
3748
|
+
else if (outcome.errno) message = `spawn ${command} ${outcome.errno}`;
|
|
3749
|
+
if (outcome.terminationUnconfirmed) message += " (termination unconfirmed)";
|
|
3750
|
+
const err = new Error(message);
|
|
3751
|
+
if (outcome.aborted) err.aborted = true;
|
|
3752
|
+
if (outcome.terminationUnconfirmed) err.terminationUnconfirmed = true;
|
|
3753
|
+
if (outcome.stderr) err.stderr = outcome.stderr;
|
|
3754
|
+
if (outcome.stdout) err.stdout = outcome.stdout;
|
|
3755
|
+
if (outcome.exitCode !== null) err.status = outcome.exitCode;
|
|
3756
|
+
if (outcome.signal !== null) err.signal = outcome.signal;
|
|
3757
|
+
if (outcome.timedOut) err.code = "ETIMEDOUT";
|
|
3758
|
+
else if (outcome.errno !== null) err.code = outcome.errno;
|
|
3759
|
+
return err;
|
|
3549
3760
|
}
|
|
3550
3761
|
|
|
3551
|
-
|
|
3552
|
-
//#region src/fixers/dependency/override-key.ts
|
|
3762
|
+
// src/fixers/dependency/override-key.ts
|
|
3553
3763
|
/**
|
|
3554
3764
|
* 归一化单个 selector。
|
|
3555
3765
|
*
|
|
@@ -3597,8 +3807,7 @@ function upsertOverride(overrides, key, value) {
|
|
|
3597
3807
|
};
|
|
3598
3808
|
}
|
|
3599
3809
|
|
|
3600
|
-
|
|
3601
|
-
//#region src/fixers/dependency/workspace-yaml.ts
|
|
3810
|
+
// src/fixers/dependency/workspace-yaml.ts
|
|
3602
3811
|
/**
|
|
3603
3812
|
* 解析 `pnpm-workspace.yaml` 文本。
|
|
3604
3813
|
*
|
|
@@ -3778,8 +3987,7 @@ function containsAlias(doc) {
|
|
|
3778
3987
|
return found;
|
|
3779
3988
|
}
|
|
3780
3989
|
|
|
3781
|
-
|
|
3782
|
-
//#region src/fixers/dependency/overrides-io.ts
|
|
3990
|
+
// src/fixers/dependency/overrides-io.ts
|
|
3783
3991
|
/**
|
|
3784
3992
|
* 执行 `pnpm install --no-frozen-lockfile`。
|
|
3785
3993
|
*
|
|
@@ -3970,8 +4178,7 @@ function formatInstallFailure(diagnostics) {
|
|
|
3970
4178
|
return hint ? `pnpm install failed: ${hint}: ${diagnostics}` : `pnpm install failed: ${diagnostics}`;
|
|
3971
4179
|
}
|
|
3972
4180
|
|
|
3973
|
-
|
|
3974
|
-
//#region src/fixers/dependency/semver.ts
|
|
4181
|
+
// src/fixers/dependency/semver.ts
|
|
3975
4182
|
/**
|
|
3976
4183
|
* 从版本声明中提取主版本号(major)。
|
|
3977
4184
|
* 去除前缀后匹配第一个 semver 段。
|
|
@@ -4041,8 +4248,7 @@ function isNonSemverDeclaration(version) {
|
|
|
4041
4248
|
return /^(workspace|catalog|link|file|npm|github|gitlab|bitbucket|gist|git(?:\+ssh|\+https|\+http|\+file)?|portal|patch|https?|ssh)\s*:/.test(trimmed);
|
|
4042
4249
|
}
|
|
4043
4250
|
|
|
4044
|
-
|
|
4045
|
-
//#region src/fixers/dependency/lockfile-versions.ts
|
|
4251
|
+
// src/fixers/dependency/lockfile-versions.ts
|
|
4046
4252
|
/**
|
|
4047
4253
|
* 从 `pnpm-lock.yaml` 中读取指定包的**所有**锁定版本实例。
|
|
4048
4254
|
*
|
|
@@ -4090,8 +4296,7 @@ function escapeRegExp(str) {
|
|
|
4090
4296
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4091
4297
|
}
|
|
4092
4298
|
|
|
4093
|
-
|
|
4094
|
-
//#region src/fixers/dependency/override-verification.ts
|
|
4299
|
+
// src/fixers/dependency/override-verification.ts
|
|
4095
4300
|
/**
|
|
4096
4301
|
* 返回仍低于目标的锁定实例(空数组 = 全部达到目标 / lockfile 中无该包)。
|
|
4097
4302
|
*
|
|
@@ -4132,8 +4337,7 @@ function extractTargetVersion(range) {
|
|
|
4132
4337
|
return /(\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?)/.exec(range)?.[1];
|
|
4133
4338
|
}
|
|
4134
4339
|
|
|
4135
|
-
|
|
4136
|
-
//#region src/fixers/dependency/manifest.ts
|
|
4340
|
+
// src/fixers/dependency/manifest.ts
|
|
4137
4341
|
/**
|
|
4138
4342
|
* 在 `package.json` 的 `dependencies` / `devDependencies` / `optionalDependencies` 中按顺序查找包。
|
|
4139
4343
|
*/
|
|
@@ -4177,8 +4381,7 @@ function ensurePnpmOverrides(pkg) {
|
|
|
4177
4381
|
return pkg.pnpm.overrides;
|
|
4178
4382
|
}
|
|
4179
4383
|
|
|
4180
|
-
|
|
4181
|
-
//#region src/fixers/dependency/manifest-upgrades.ts
|
|
4384
|
+
// src/fixers/dependency/manifest-upgrades.ts
|
|
4182
4385
|
async function applyManifestUpgrades(params) {
|
|
4183
4386
|
const { workDir, manifestDir, requests } = params;
|
|
4184
4387
|
const pkgPath = join(workDir, manifestDir ?? ".", "package.json");
|
|
@@ -4307,8 +4510,7 @@ async function applyManifestUpgrades(params) {
|
|
|
4307
4510
|
};
|
|
4308
4511
|
}
|
|
4309
4512
|
|
|
4310
|
-
|
|
4311
|
-
//#region src/fixers/dependency/index.ts
|
|
4513
|
+
// src/fixers/dependency/index.ts
|
|
4312
4514
|
/**
|
|
4313
4515
|
* 升级单个依赖到指定版本。
|
|
4314
4516
|
*
|
|
@@ -4780,8 +4982,7 @@ function isCrossMajorFixRequired(lockfilePath, alert) {
|
|
|
4780
4982
|
return !versions.some((v) => parseMajorVersion(v) === targetMajor);
|
|
4781
4983
|
}
|
|
4782
4984
|
|
|
4783
|
-
|
|
4784
|
-
//#region src/fixers/pnpm/index.ts
|
|
4985
|
+
// src/fixers/pnpm/index.ts
|
|
4785
4986
|
/**
|
|
4786
4987
|
* 执行 `pnpm i --frozen-lockfile` 并解析失败分类。
|
|
4787
4988
|
* 成功时返回 `{ ok: true }`。
|
|
@@ -5266,8 +5467,7 @@ function readLockfileVersionOf(workDir) {
|
|
|
5266
5467
|
}
|
|
5267
5468
|
}
|
|
5268
5469
|
|
|
5269
|
-
|
|
5270
|
-
//#region src/fixers/code-scanning/index.ts
|
|
5470
|
+
// src/fixers/code-scanning/index.ts
|
|
5271
5471
|
/**
|
|
5272
5472
|
* 应用单个 Code Scanning 告警的模板修复。
|
|
5273
5473
|
*
|
|
@@ -5278,7 +5478,13 @@ function applyCodeScanningFix(params) {
|
|
|
5278
5478
|
const { workDir, alert, dryRun } = params;
|
|
5279
5479
|
if (alert.source !== "code-scanning" || alert.alertClass !== "auto-fixable") return null;
|
|
5280
5480
|
const startMs = Date.now();
|
|
5281
|
-
/**
|
|
5481
|
+
/**
|
|
5482
|
+
* 无法安全处理(陈旧告警/缺模板/歧义)→ noOp 标记,不计 failed(避免永久 exit 1/2)。
|
|
5483
|
+
*
|
|
5484
|
+
* ⚠️ 与 `packages/core/src/report/alert-outcomes.ts#isAlreadyCompliant` 成对:该函数把
|
|
5485
|
+
* 「`success + noOp` 且**无 `error`**」判为已收敛,因此本函数产出的每个 skip 都必须带 `error`
|
|
5486
|
+
* (compliant 分支不设 `error`)。新增 skip 分支漏写 `error` 会把「无法处理」写成 `converged`。
|
|
5487
|
+
*/
|
|
5282
5488
|
const skip = (reason) => ({
|
|
5283
5489
|
type: "code-scanning-fix",
|
|
5284
5490
|
repository: alert.repository,
|
|
@@ -5429,8 +5635,7 @@ function resolveWithinWorkDir(workDir, relativePath) {
|
|
|
5429
5635
|
}
|
|
5430
5636
|
}
|
|
5431
5637
|
|
|
5432
|
-
|
|
5433
|
-
//#region src/multirepo/scheduler.ts
|
|
5638
|
+
// src/multirepo/scheduler.ts
|
|
5434
5639
|
/** 并发上限(1-16,配置校验范围) */
|
|
5435
5640
|
const MAX_CONCURRENCY_LIMIT = 16;
|
|
5436
5641
|
/** 默认并发(保守:行为与现状一致,逐仓库串行) */
|
|
@@ -5461,8 +5666,7 @@ function isValidConcurrency(value) {
|
|
|
5461
5666
|
return Number.isInteger(value) && value >= 1 && value <= 16;
|
|
5462
5667
|
}
|
|
5463
5668
|
|
|
5464
|
-
|
|
5465
|
-
//#region src/config/validation.ts
|
|
5669
|
+
// src/config/validation.ts
|
|
5466
5670
|
/**
|
|
5467
5671
|
* 运行时配置校验(从 `config/index.ts` 拆分以控制单文件行数)。
|
|
5468
5672
|
*
|
|
@@ -5512,9 +5716,24 @@ function validateRuntimeConfig(config) {
|
|
|
5512
5716
|
function validateGitHubAppConfig(config) {
|
|
5513
5717
|
if (!config.appId || !config.privateKey || !config.installationId) throw new AppError("CONFIG_VALIDATION_ERROR", "GitHub App config requires appId, privateKey, and installationId");
|
|
5514
5718
|
}
|
|
5719
|
+
const BASELINE_RED_DELIVERIES = [
|
|
5720
|
+
"skip",
|
|
5721
|
+
"issue",
|
|
5722
|
+
"draft"
|
|
5723
|
+
];
|
|
5724
|
+
/**
|
|
5725
|
+
* 解析 `DEPENDFIX_ON_BASELINE_RED`(`skip` / `issue` / `draft`)。
|
|
5726
|
+
*
|
|
5727
|
+
* 未设置 / 空串 → `undefined`(由默认值补 `skip`);非法值 fail-fast。
|
|
5728
|
+
*/
|
|
5729
|
+
function readBaselineRedDelivery(value, fieldName) {
|
|
5730
|
+
if (value === void 0 || value.trim() === "") return;
|
|
5731
|
+
const normalized = value.trim().toLowerCase();
|
|
5732
|
+
if (!BASELINE_RED_DELIVERIES.includes(normalized)) throw new AppError("CONFIG_VALIDATION_ERROR", `${fieldName} must be one of: ${BASELINE_RED_DELIVERIES.join(", ")}`);
|
|
5733
|
+
return normalized;
|
|
5734
|
+
}
|
|
5515
5735
|
|
|
5516
|
-
|
|
5517
|
-
//#region src/config/index.ts
|
|
5736
|
+
// src/config/index.ts
|
|
5518
5737
|
const RUNTIME_MODES = [
|
|
5519
5738
|
"report-only",
|
|
5520
5739
|
"fix",
|
|
@@ -5551,6 +5770,7 @@ const DEFAULT_RUNTIME_CONFIG = {
|
|
|
5551
5770
|
rollbackGranularity: "unit",
|
|
5552
5771
|
mirrorCiScripts: false,
|
|
5553
5772
|
retryFailed: false,
|
|
5773
|
+
onBaselineRed: "skip",
|
|
5554
5774
|
waitPrChecks: false,
|
|
5555
5775
|
closeFailedPr: false,
|
|
5556
5776
|
prCheckTimeoutMs: 9e5,
|
|
@@ -5703,6 +5923,7 @@ function readEnvConfig(env = process.env) {
|
|
|
5703
5923
|
rollbackGranularity: readRollbackGranularity(readEnv(env, "ROLLBACK_GRANULARITY"), `${ENV_PREFIX}ROLLBACK_GRANULARITY`),
|
|
5704
5924
|
mirrorCiScripts: normalizeBoolean(readEnv(env, "MIRROR_CI_SCRIPTS"), `${ENV_PREFIX}MIRROR_CI_SCRIPTS`),
|
|
5705
5925
|
retryFailed: normalizeBoolean(readEnv(env, "RETRY_FAILED"), `${ENV_PREFIX}RETRY_FAILED`),
|
|
5926
|
+
onBaselineRed: readBaselineRedDelivery(readEnv(env, "ON_BASELINE_RED"), `${ENV_PREFIX}ON_BASELINE_RED`),
|
|
5706
5927
|
waitPrChecks: normalizeBoolean(readEnv(env, "WAIT_PR_CHECKS"), `${ENV_PREFIX}WAIT_PR_CHECKS`),
|
|
5707
5928
|
closeFailedPr: normalizeBoolean(readEnv(env, "CLOSE_FAILED_PR"), `${ENV_PREFIX}CLOSE_FAILED_PR`),
|
|
5708
5929
|
prCheckTimeoutMs: normalizeInteger(readEnv(env, "PR_CHECK_TIMEOUT_MS"), `${ENV_PREFIX}PR_CHECK_TIMEOUT_MS`),
|
|
@@ -5809,6 +6030,7 @@ function resolveRuntimeConfig(options = {}) {
|
|
|
5809
6030
|
rollbackGranularity: cliOverrides.rollbackGranularity ?? envConfig.rollbackGranularity ?? DEFAULT_RUNTIME_CONFIG.rollbackGranularity,
|
|
5810
6031
|
mirrorCiScripts: cliOverrides.mirrorCiScripts ?? envConfig.mirrorCiScripts ?? DEFAULT_RUNTIME_CONFIG.mirrorCiScripts,
|
|
5811
6032
|
retryFailed: cliOverrides.retryFailed ?? envConfig.retryFailed ?? DEFAULT_RUNTIME_CONFIG.retryFailed,
|
|
6033
|
+
onBaselineRed: cliOverrides.onBaselineRed ?? envConfig.onBaselineRed ?? DEFAULT_RUNTIME_CONFIG.onBaselineRed,
|
|
5812
6034
|
waitPrChecks: cliOverrides.waitPrChecks ?? envConfig.waitPrChecks ?? DEFAULT_RUNTIME_CONFIG.waitPrChecks,
|
|
5813
6035
|
closeFailedPr: cliOverrides.closeFailedPr ?? envConfig.closeFailedPr ?? DEFAULT_RUNTIME_CONFIG.closeFailedPr,
|
|
5814
6036
|
prCheckTimeoutMs: cliOverrides.prCheckTimeoutMs ?? envConfig.prCheckTimeoutMs ?? DEFAULT_RUNTIME_CONFIG.prCheckTimeoutMs,
|
|
@@ -5865,8 +6087,7 @@ function inferRepoFromGitRemote(workDir) {
|
|
|
5865
6087
|
}
|
|
5866
6088
|
}
|
|
5867
6089
|
|
|
5868
|
-
|
|
5869
|
-
//#region src/report/archiver.ts
|
|
6090
|
+
// src/report/archiver.ts
|
|
5870
6091
|
/**
|
|
5871
6092
|
* 归档一次运行结果:
|
|
5872
6093
|
* - `{outputDir}/{YYYY-MM}/{runId}/summary.json`:全局 RunResult 汇总(同字段口径)
|
|
@@ -6010,8 +6231,7 @@ function buildArchiveEntry(runResult) {
|
|
|
6010
6231
|
};
|
|
6011
6232
|
}
|
|
6012
6233
|
|
|
6013
|
-
|
|
6014
|
-
//#region src/report/history.ts
|
|
6234
|
+
// src/report/history.ts
|
|
6015
6235
|
/**
|
|
6016
6236
|
* 将某仓库的历史运行摘要格式化为多行文本(倒序时间:最新在前)。
|
|
6017
6237
|
*
|
|
@@ -6033,8 +6253,7 @@ function formatHistory(entries, repo) {
|
|
|
6033
6253
|
return lines.join("\n");
|
|
6034
6254
|
}
|
|
6035
6255
|
|
|
6036
|
-
|
|
6037
|
-
//#region src/runners/network-audit.ts
|
|
6256
|
+
// src/runners/network-audit.ts
|
|
6038
6257
|
const CONNECT_TIMEOUT_MS = 1e4;
|
|
6039
6258
|
/**
|
|
6040
6259
|
* 默认出站白名单(网络白名单治理决策:registry + 常见镜像站 + GitHub API/资产域 + 受信任构建工具文档站)。
|
|
@@ -6289,8 +6508,7 @@ function logNetworkAudit(logger, repo, entries) {
|
|
|
6289
6508
|
for (const entry of entries) logger.debug(`[network-audit] ${repo}: ${entry.source} ${entry.method} ${entry.target}`);
|
|
6290
6509
|
}
|
|
6291
6510
|
|
|
6292
|
-
|
|
6293
|
-
//#region src/runners/registry-discovery.ts
|
|
6511
|
+
// src/runners/registry-discovery.ts
|
|
6294
6512
|
/** 单次配置读取超时(只读本地配置,正常毫秒级完成;超时即 fail-open) */
|
|
6295
6513
|
const DEFAULT_TIMEOUT_MS$2 = 5e3;
|
|
6296
6514
|
/**
|
|
@@ -6433,8 +6651,7 @@ function discoverRegistryHostsUncached(workDir, options) {
|
|
|
6433
6651
|
};
|
|
6434
6652
|
}
|
|
6435
6653
|
|
|
6436
|
-
|
|
6437
|
-
//#region src/runners/verification-runner.ts
|
|
6654
|
+
// src/runners/verification-runner.ts
|
|
6438
6655
|
/**
|
|
6439
6656
|
* 默认验证命令链(**唯一事实源**——app 层经此导入,消除此前的双副本漂移)。
|
|
6440
6657
|
*
|
|
@@ -6741,8 +6958,7 @@ function truncateLines(text, maxLines) {
|
|
|
6741
6958
|
return `${lines.slice(0, maxLines).join("\n")}\n... (truncated, ${lines.length - maxLines} more lines)`;
|
|
6742
6959
|
}
|
|
6743
6960
|
|
|
6744
|
-
|
|
6745
|
-
//#region src/helpers/index.ts
|
|
6961
|
+
// src/helpers/index.ts
|
|
6746
6962
|
/**
|
|
6747
6963
|
* 对可修复告警按包名去重(同包收敛)。
|
|
6748
6964
|
*
|
|
@@ -7019,8 +7235,7 @@ function collectSubdirectories(dir, out) {
|
|
|
7019
7235
|
}
|
|
7020
7236
|
}
|
|
7021
7237
|
|
|
7022
|
-
|
|
7023
|
-
//#region src/grouping/index.ts
|
|
7238
|
+
// src/grouping/index.ts
|
|
7024
7239
|
/**
|
|
7025
7240
|
* 判断包名是否匹配 dependabot pattern。
|
|
7026
7241
|
*
|
|
@@ -7268,8 +7483,7 @@ function buildUpgradeGroups(alerts, options) {
|
|
|
7268
7483
|
};
|
|
7269
7484
|
}
|
|
7270
7485
|
|
|
7271
|
-
|
|
7272
|
-
//#region src/alerts/pnpm-audit-fetcher.ts
|
|
7486
|
+
// src/alerts/pnpm-audit-fetcher.ts
|
|
7273
7487
|
/**
|
|
7274
7488
|
* 执行 `pnpm audit --json` 并将风险归一化为标准化告警。
|
|
7275
7489
|
*
|
|
@@ -7540,8 +7754,7 @@ function mapAuditRiskToAlert(risk, repository) {
|
|
|
7540
7754
|
};
|
|
7541
7755
|
}
|
|
7542
7756
|
|
|
7543
|
-
|
|
7544
|
-
//#region src/ai/secrets.ts
|
|
7757
|
+
// src/ai/secrets.ts
|
|
7545
7758
|
/**
|
|
7546
7759
|
* 对文本中的敏感值做脱敏替换(保留首 4 尾 4 字符便于定位)。
|
|
7547
7760
|
*
|
|
@@ -7567,8 +7780,7 @@ function maskValue(secret) {
|
|
|
7567
7780
|
return `${secret.slice(0, 4)}****${secret.slice(-4)}`;
|
|
7568
7781
|
}
|
|
7569
7782
|
|
|
7570
|
-
|
|
7571
|
-
//#region src/ai/provider.ts
|
|
7783
|
+
// src/ai/provider.ts
|
|
7572
7784
|
/**
|
|
7573
7785
|
* AI 调用失败(HTTP 非 2xx / 网络错误)。
|
|
7574
7786
|
* message 已在本层脱敏(maskSecrets,不含 apiKey)——防御纵深,
|
|
@@ -7707,8 +7919,7 @@ function truncate(text, max) {
|
|
|
7707
7919
|
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
7708
7920
|
}
|
|
7709
7921
|
|
|
7710
|
-
|
|
7711
|
-
//#region src/ai/schema.ts
|
|
7922
|
+
// src/ai/schema.ts
|
|
7712
7923
|
const replaceBlockSchema = z.object({
|
|
7713
7924
|
/**
|
|
7714
7925
|
* 需匹配的原文(唯一性校验在应用层:同文件多块必须精确匹配且互不重叠)。
|
|
@@ -7790,8 +8001,7 @@ function extractJson(text) {
|
|
|
7790
8001
|
return block ? block[1].trim() : null;
|
|
7791
8002
|
}
|
|
7792
8003
|
|
|
7793
|
-
|
|
7794
|
-
//#region src/ai/prompt.ts
|
|
8004
|
+
// src/ai/prompt.ts
|
|
7795
8005
|
/** system prompt(硬编码常量;禁止从任何用户输入拼接) */
|
|
7796
8006
|
const ASSESSMENT_SYSTEM_PROMPT = `你是依赖升级兼容性(breaking change)研判助手。你的任务是根据升级上下文判断依赖升级后的不兼容问题,并输出结构化修复方案。
|
|
7797
8007
|
|
|
@@ -7853,8 +8063,7 @@ function truncateChars(text, max) {
|
|
|
7853
8063
|
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
7854
8064
|
}
|
|
7855
8065
|
|
|
7856
|
-
|
|
7857
|
-
//#region src/ai/usage.ts
|
|
8066
|
+
// src/ai/usage.ts
|
|
7858
8067
|
/** 每百万 token 单价(USD,公开定价推算;DeepSeek 为官方价格,其余为常见模型价) */
|
|
7859
8068
|
const MODEL_PRICE_TABLE = {
|
|
7860
8069
|
"gpt-4o-mini": {
|
|
@@ -7932,8 +8141,7 @@ var AiUsageTracker = class {
|
|
|
7932
8141
|
}
|
|
7933
8142
|
};
|
|
7934
8143
|
|
|
7935
|
-
|
|
7936
|
-
//#region src/ai/index.ts
|
|
8144
|
+
// src/ai/index.ts
|
|
7937
8145
|
const DEFAULT_SCHEMA_RETRIES = 1;
|
|
7938
8146
|
/**
|
|
7939
8147
|
* 执行一次 breaking change 研判。
|
|
@@ -8000,8 +8208,7 @@ async function assessBreakingChange(options) {
|
|
|
8000
8208
|
};
|
|
8001
8209
|
}
|
|
8002
8210
|
|
|
8003
|
-
|
|
8004
|
-
//#region src/app/delivery.ts
|
|
8211
|
+
// src/app/delivery.ts
|
|
8005
8212
|
/**
|
|
8006
8213
|
* 安装类命令判定(与 `verify-project.ts` 的基线安装失败识别同口径)。
|
|
8007
8214
|
*
|
|
@@ -8050,9 +8257,33 @@ function findInstallBlockedReposWithFixes(actions) {
|
|
|
8050
8257
|
const fixRepos = new Set(actions.filter(isDeliverableFix).map((action) => action.repository));
|
|
8051
8258
|
return [...new Set(actions.filter(isPreExistingInstallFailure).map((action) => action.repository).filter((repo) => fixRepos.has(repo)))];
|
|
8052
8259
|
}
|
|
8260
|
+
/**
|
|
8261
|
+
* 收集给定仓库上「既有失败的安装命令」(去重,保持执行顺序)。
|
|
8262
|
+
*
|
|
8263
|
+
* 报告与交付降级 issue 正文只引用命令本身:错误文本可能含凭据 / 路径,且会让
|
|
8264
|
+
* 失败指纹随日志抖动漂移(见 [pr-delivery-closure.md §16])。
|
|
8265
|
+
*/
|
|
8266
|
+
function collectFailingInstallCommands(actions, repositories) {
|
|
8267
|
+
const scoped = new Set(repositories);
|
|
8268
|
+
const commands = [];
|
|
8269
|
+
for (const action of actions) {
|
|
8270
|
+
if (!scoped.has(action.repository) || !isPreExistingInstallFailure(action)) continue;
|
|
8271
|
+
if (!commands.includes(action.target)) commands.push(action.target);
|
|
8272
|
+
}
|
|
8273
|
+
return commands;
|
|
8274
|
+
}
|
|
8275
|
+
/**
|
|
8276
|
+
* 记录一次交付跳过(run 级;`summary.deliverySkipped*` 由 `computeSummary` 据此派生)。
|
|
8277
|
+
*
|
|
8278
|
+
* 入参用结构化最小面(而非 `AppContext`)以避免 `helpers.ts ↔ delivery.ts` 循环依赖;
|
|
8279
|
+
* 返回同一条记录引用,便于调用方创建载体后回填编号。
|
|
8280
|
+
*/
|
|
8281
|
+
function recordDeliverySkip(target, record) {
|
|
8282
|
+
(target.deliverySkips ?? (target.deliverySkips = [])).push(record);
|
|
8283
|
+
return record;
|
|
8284
|
+
}
|
|
8053
8285
|
|
|
8054
|
-
|
|
8055
|
-
//#region src/app/branch-cleanup.ts
|
|
8286
|
+
// src/app/branch-cleanup.ts
|
|
8056
8287
|
/**
|
|
8057
8288
|
* 清理单个仓库的 dependfix 分支(cleanup-branches 模式循环体)。
|
|
8058
8289
|
* 分类:已合并(安全清理)/ 已关闭未合并(supersede 孤儿)/ open(跳过);
|
|
@@ -8261,8 +8492,7 @@ function confirmCleanup(ctx, repo, candidates) {
|
|
|
8261
8492
|
});
|
|
8262
8493
|
}
|
|
8263
8494
|
|
|
8264
|
-
|
|
8265
|
-
//#region src/app/result-assembly.ts
|
|
8495
|
+
// src/app/result-assembly.ts
|
|
8266
8496
|
/**
|
|
8267
8497
|
* 汇总所有动作到 summary(alertsSkipped / alertsConverged 已在 repo-fix 修复管线中累加)。
|
|
8268
8498
|
*
|
|
@@ -8307,6 +8537,10 @@ function computeSummary(ctx) {
|
|
|
8307
8537
|
summary.lockfileRepairs = lockfileRepairs;
|
|
8308
8538
|
summary.verificationsPassed = verificationsPassed;
|
|
8309
8539
|
summary.verificationsFailed = verificationsFailed;
|
|
8540
|
+
const deliverySkips = ctx.deliverySkips ?? [];
|
|
8541
|
+
summary.deliverySkipped = new Set(deliverySkips.flatMap((record) => record.repositories)).size;
|
|
8542
|
+
summary.deliverySkipIssues = deliverySkips.filter((record) => record.carrier.issue?.created === true).length;
|
|
8543
|
+
summary.deliverySkipDraftPrs = deliverySkips.filter((record) => Boolean(record.carrier.draftPr)).length;
|
|
8310
8544
|
}
|
|
8311
8545
|
function buildRunResult(ctx, aiUsage, supplyChainWarnings) {
|
|
8312
8546
|
const reportConfig = {
|
|
@@ -8320,6 +8554,7 @@ function buildRunResult(ctx, aiUsage, supplyChainWarnings) {
|
|
|
8320
8554
|
codeScanningEnabled: ctx.config.codeScanningEnabled,
|
|
8321
8555
|
codeQualityEnabled: ctx.config.codeQualityEnabled
|
|
8322
8556
|
};
|
|
8557
|
+
const alertOutcomes = buildAlertOutcomes(ctx.allAlerts, ctx.allActions, ctx.alertOutcomes);
|
|
8323
8558
|
return {
|
|
8324
8559
|
runId: ctx.runId,
|
|
8325
8560
|
startedAt: ctx.startedAt,
|
|
@@ -8335,7 +8570,9 @@ function buildRunResult(ctx, aiUsage, supplyChainWarnings) {
|
|
|
8335
8570
|
alertsDisabled: ctx.alertsDisabled.length > 0 ? ctx.alertsDisabled : void 0,
|
|
8336
8571
|
ciAlignment: ctx.ciAlignment && ctx.ciAlignment.length > 0 ? ctx.ciAlignment : void 0,
|
|
8337
8572
|
attemptTracking: ctx.attemptTracking && ctx.attemptTracking.length > 0 ? ctx.attemptTracking : void 0,
|
|
8338
|
-
|
|
8573
|
+
alertOutcomes: alertOutcomes.length > 0 ? alertOutcomes : void 0,
|
|
8574
|
+
prCheckWatch: ctx.prCheckWatch && ctx.prCheckWatch.attempts.length > 0 ? ctx.prCheckWatch : void 0,
|
|
8575
|
+
deliverySkips: ctx.deliverySkips && ctx.deliverySkips.length > 0 ? ctx.deliverySkips : void 0
|
|
8339
8576
|
};
|
|
8340
8577
|
}
|
|
8341
8578
|
/**
|
|
@@ -8373,7 +8610,8 @@ const SKIPPED_AUDIT_CATEGORIES = /* @__PURE__ */ new Set([
|
|
|
8373
8610
|
"SCRIPT_NOT_FOUND",
|
|
8374
8611
|
"PRE_EXISTING_FAILURE",
|
|
8375
8612
|
"ENVIRONMENT_ERROR",
|
|
8376
|
-
"ROLLBACK_GRANULARITY_DEGRADED"
|
|
8613
|
+
"ROLLBACK_GRANULARITY_DEGRADED",
|
|
8614
|
+
"BUDGET_EXHAUSTED"
|
|
8377
8615
|
]);
|
|
8378
8616
|
/**
|
|
8379
8617
|
* 计算退出码:
|
|
@@ -8400,8 +8638,7 @@ function computeExitCode(ctx) {
|
|
|
8400
8638
|
return 2;
|
|
8401
8639
|
}
|
|
8402
8640
|
|
|
8403
|
-
|
|
8404
|
-
//#region src/app/helpers.ts
|
|
8641
|
+
// src/app/helpers.ts
|
|
8405
8642
|
/** 匹配 `pnpm <singleWord>` 模式的命令(可能是 package.json script 引用) */
|
|
8406
8643
|
/**
|
|
8407
8644
|
* 解析本次运行要处理的仓库列表。
|
|
@@ -8726,7 +8963,7 @@ async function tryLockfileRepair(ctx, repo) {
|
|
|
8726
8963
|
* - 提交集合由白名单 + fixer 声明路径决定(`stageAndCommit`);含未声明的已跟踪改动时抛
|
|
8727
8964
|
* `CommitScopeError('UNEXPECTED_CHANGES')`,仅供生成物 / 未跟踪文件时抛 `NO_CHANGES`
|
|
8728
8965
|
*/
|
|
8729
|
-
function commitLocalChanges(ctx) {
|
|
8966
|
+
async function commitLocalChanges(ctx) {
|
|
8730
8967
|
const { logger, workDir, allActions, authProvider } = ctx;
|
|
8731
8968
|
if (!isGitRepository(workDir)) {
|
|
8732
8969
|
logger.warn(`工作目录不是 git 仓库 — 跳过本地提交(${workDir})`);
|
|
@@ -8738,6 +8975,11 @@ function commitLocalChanges(ctx) {
|
|
|
8738
8975
|
}
|
|
8739
8976
|
const installBlockedRepos = findInstallBlockedReposWithFixes(allActions);
|
|
8740
8977
|
if (installBlockedRepos.length > 0) {
|
|
8978
|
+
recordDeliverySkip(ctx, {
|
|
8979
|
+
repositories: installBlockedRepos,
|
|
8980
|
+
failingCommands: collectFailingInstallCommands(allActions, installBlockedRepos),
|
|
8981
|
+
carrier: { mode: "skip" }
|
|
8982
|
+
});
|
|
8741
8983
|
logger.warn(`Install verification is still failing (pre-existing) after the fix for ${installBlockedRepos.join(", ")} — skipping local commit; fix the execution environment or target repository lockfile first`);
|
|
8742
8984
|
return;
|
|
8743
8985
|
}
|
|
@@ -8748,7 +8990,7 @@ function commitLocalChanges(ctx) {
|
|
|
8748
8990
|
ensureGitignore(workDir);
|
|
8749
8991
|
const commitMessage = buildCommitMessage(allActions);
|
|
8750
8992
|
const author = authProvider?.getCommitAuthor();
|
|
8751
|
-
const scope = stageAndCommit(commitMessage, workDir, author, { declaredPaths: collectDeclaredPaths(allActions) });
|
|
8993
|
+
const scope = await stageAndCommit(commitMessage, workDir, author, { declaredPaths: collectDeclaredPaths(allActions) });
|
|
8752
8994
|
if (scope.ignored.length > 0) logger.info(`Not committing ${scope.ignored.length} generated / undeclared path(s): ${scope.ignored.join(", ")}`);
|
|
8753
8995
|
if (scope.missing.length > 0) logger.warn(`Declared path(s) had no changes in the worktree: ${scope.missing.join(", ")}`);
|
|
8754
8996
|
logger.info(`Committed fix changes to current branch: ${commitMessage.split("\n")[0]}`);
|
|
@@ -8866,8 +9108,7 @@ function buildPrTitle(summary, actions) {
|
|
|
8866
9108
|
return parts.length > 0 ? `chore(deps): automated security fix — ${parts.join(", ")}` : "chore(deps): automated security fix";
|
|
8867
9109
|
}
|
|
8868
9110
|
|
|
8869
|
-
|
|
8870
|
-
//#region src/runners/verification-gate.ts
|
|
9111
|
+
// src/runners/verification-gate.ts
|
|
8871
9112
|
/**
|
|
8872
9113
|
* 单仓库是否被「本次改动引入的验证失败」阻断。
|
|
8873
9114
|
*
|
|
@@ -8894,10 +9135,14 @@ function findVerificationFailedRepos(repoResults) {
|
|
|
8894
9135
|
* - 仅限验证失败且尚未提交的场景;不清理未跟踪文件(避免误删用户文件;
|
|
8895
9136
|
* node_modules 等未跟踪目录保留,下次 install 自愈)
|
|
8896
9137
|
*/
|
|
8897
|
-
function rollbackChanges(workDir) {
|
|
8898
|
-
|
|
9138
|
+
async function rollbackChanges(workDir) {
|
|
9139
|
+
await runGitOrThrow([
|
|
9140
|
+
"reset",
|
|
9141
|
+
"--hard",
|
|
9142
|
+
"HEAD"
|
|
9143
|
+
], {
|
|
8899
9144
|
cwd: workDir,
|
|
8900
|
-
|
|
9145
|
+
what: "rollback worktree"
|
|
8901
9146
|
});
|
|
8902
9147
|
}
|
|
8903
9148
|
/**
|
|
@@ -8917,10 +9162,14 @@ function rollbackChanges(workDir) {
|
|
|
8917
9162
|
* - 多仓库共享 workDir 时文件级改动无法按仓拆分;选择性回滚按当前仓库最近 checkpoint 处理,
|
|
8918
9163
|
* 失败时显式记录 `ROLLBACK_GRANULARITY_DEGRADED` 后退回整仓回滚
|
|
8919
9164
|
*/
|
|
8920
|
-
function enforceVerificationGate(ctx, options) {
|
|
9165
|
+
async function enforceVerificationGate(ctx, options) {
|
|
8921
9166
|
const failedRepos = findVerificationFailedRepos(ctx.repoResults);
|
|
8922
9167
|
if (failedRepos.length === 0) return false;
|
|
8923
9168
|
const { logger, workDir, allErrors } = ctx;
|
|
9169
|
+
if (failedRepos.every((repo) => ctx.repoResults.find((result) => result.repository === repo)?.verificationSkipped === "budget")) {
|
|
9170
|
+
logger.warn(`Verification skipped by run budget for: ${failedRepos.join(", ")} — delivery skipped (worktree preserved; no changes were rolled back)`);
|
|
9171
|
+
return true;
|
|
9172
|
+
}
|
|
8924
9173
|
const actionLabel = options.action === "pr" ? "PR creation" : "local commit";
|
|
8925
9174
|
const repoList = failedRepos.join(", ");
|
|
8926
9175
|
logger.error(`Verification failed for: ${repoList} — skipping ${actionLabel}`);
|
|
@@ -8939,9 +9188,13 @@ function enforceVerificationGate(ctx, options) {
|
|
|
8939
9188
|
return true;
|
|
8940
9189
|
}
|
|
8941
9190
|
try {
|
|
8942
|
-
rollbackChanges(workDir);
|
|
9191
|
+
await rollbackChanges(workDir);
|
|
8943
9192
|
logger.info("Changes rolled back");
|
|
8944
9193
|
} catch (error) {
|
|
9194
|
+
if (error instanceof GitCommandError && error.aborted && !error.started) {
|
|
9195
|
+
logger.warn("Run aborted before rollback started — worktree left as-is (caller cleans up)");
|
|
9196
|
+
return true;
|
|
9197
|
+
}
|
|
8945
9198
|
const message = toErrorMessage(error);
|
|
8946
9199
|
logger.error(`Rollback failed: ${message}`);
|
|
8947
9200
|
allErrors.push({
|
|
@@ -8954,8 +9207,7 @@ function enforceVerificationGate(ctx, options) {
|
|
|
8954
9207
|
return true;
|
|
8955
9208
|
}
|
|
8956
9209
|
|
|
8957
|
-
|
|
8958
|
-
//#region src/supply-chain/index.ts
|
|
9210
|
+
// src/supply-chain/index.ts
|
|
8959
9211
|
/**
|
|
8960
9212
|
* 供应链信号披露(安全治理:路径 A"合法包被投毒"合入前人工确认的关键依据)。
|
|
8961
9213
|
*
|
|
@@ -9128,8 +9380,7 @@ function unquote(value) {
|
|
|
9128
9380
|
return value;
|
|
9129
9381
|
}
|
|
9130
9382
|
|
|
9131
|
-
|
|
9132
|
-
//#region src/app/github-client.ts
|
|
9383
|
+
// src/app/github-client.ts
|
|
9133
9384
|
/** 构造主 token client(429 / 限流按配置指数退避)。 */
|
|
9134
9385
|
function createConfiguredGitHubClient(config) {
|
|
9135
9386
|
return createGitHubClient({ auth: fromPat(config.githubToken, { retry: {
|
|
@@ -9142,8 +9393,7 @@ function createConfiguredGitHubClientOrNull(config) {
|
|
|
9142
9393
|
return config.alertSource === "pnpm-audit" ? null : createConfiguredGitHubClient(config);
|
|
9143
9394
|
}
|
|
9144
9395
|
|
|
9145
|
-
|
|
9146
|
-
//#region src/app/build-info.ts
|
|
9396
|
+
// src/app/build-info.ts
|
|
9147
9397
|
/**
|
|
9148
9398
|
* 运行构建版本回执:回答「本次运行由哪个构建版本 / commit 执行」。
|
|
9149
9399
|
*
|
|
@@ -9210,8 +9460,7 @@ const resolveRunBuildInfo = (config = {}, deps = {}) => {
|
|
|
9210
9460
|
*/
|
|
9211
9461
|
const formatEngineBuildLine = (info) => `[build] dependfix version=${info.version} commit=${info.commit} source=${info.source}`;
|
|
9212
9462
|
|
|
9213
|
-
|
|
9214
|
-
//#region src/app/token-hints.ts
|
|
9463
|
+
// src/app/token-hints.ts
|
|
9215
9464
|
/**
|
|
9216
9465
|
* PR 创建失败的用户指引。
|
|
9217
9466
|
* 识别 GITHUB_TOKEN 创建 PR 被仓库设置禁用的 403("GitHub Actions is not
|
|
@@ -9309,8 +9558,7 @@ function alertsFetchTokenHint(error) {
|
|
|
9309
9558
|
return dependabotAlertsTokenHint(error) ?? codeScanningAlertsTokenHint(error) ?? codeQualityAlertsTokenHint(error);
|
|
9310
9559
|
}
|
|
9311
9560
|
|
|
9312
|
-
|
|
9313
|
-
//#region src/app/repo-alerts.ts
|
|
9561
|
+
// src/app/repo-alerts.ts
|
|
9314
9562
|
/**
|
|
9315
9563
|
* 告警数据源统一入口:
|
|
9316
9564
|
* - `github-dependabot`:Octokit 拉取 Dependabot alerts(alertsToken 优先);
|
|
@@ -9453,8 +9701,7 @@ function truncatedWarning(config, truncatedCount) {
|
|
|
9453
9701
|
return config.codeScanningEnabled ? `${base}; code-scanning alerts rank after fixable dependabot alerts` : base;
|
|
9454
9702
|
}
|
|
9455
9703
|
|
|
9456
|
-
|
|
9457
|
-
//#region src/app/abort.ts
|
|
9704
|
+
// src/app/abort.ts
|
|
9458
9705
|
/**
|
|
9459
9706
|
* 记录一次运行级中止(幂等:同一运行的重复调用不再重复留痕)。
|
|
9460
9707
|
*
|
|
@@ -9502,8 +9749,7 @@ function abortBlocksDelivery(ctx, action) {
|
|
|
9502
9749
|
return true;
|
|
9503
9750
|
}
|
|
9504
9751
|
|
|
9505
|
-
|
|
9506
|
-
//#region src/app/repo-report.ts
|
|
9752
|
+
// src/app/repo-report.ts
|
|
9507
9753
|
/**
|
|
9508
9754
|
* 处理单个仓库的 report-only 抓取,把告警与仓库结果写入上下文。
|
|
9509
9755
|
*
|
|
@@ -9558,8 +9804,7 @@ async function processRepoForReport(ctx, client, repo) {
|
|
|
9558
9804
|
}
|
|
9559
9805
|
}
|
|
9560
9806
|
|
|
9561
|
-
|
|
9562
|
-
//#region src/app/override-protect.ts
|
|
9807
|
+
// src/app/override-protect.ts
|
|
9563
9808
|
/**
|
|
9564
9809
|
* 判定 overrides 保护名单;命中则记审计并返回 noOp 动作,未命中返回 null。
|
|
9565
9810
|
*
|
|
@@ -9594,8 +9839,7 @@ function handleOverrideProtection(ctx, params) {
|
|
|
9594
9839
|
};
|
|
9595
9840
|
}
|
|
9596
9841
|
|
|
9597
|
-
|
|
9598
|
-
//#region src/fixers/dependency/families.ts
|
|
9842
|
+
// src/fixers/dependency/families.ts
|
|
9599
9843
|
/**
|
|
9600
9844
|
* 首批家族清单(2026-10-24 巡检驱动)。
|
|
9601
9845
|
*
|
|
@@ -9873,8 +10117,7 @@ function rangeAllows(version, range) {
|
|
|
9873
10117
|
}
|
|
9874
10118
|
}
|
|
9875
10119
|
|
|
9876
|
-
|
|
9877
|
-
//#region src/fixers/dependency/family-versions.ts
|
|
10120
|
+
// src/fixers/dependency/family-versions.ts
|
|
9878
10121
|
const DEFAULT_REGISTRY_BASE_URL = "https://registry.npmjs.org";
|
|
9879
10122
|
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
9880
10123
|
/** 单个 resolver 缓存上限(包数量级,超限整体清空,防长驻进程无界增长) */
|
|
@@ -10033,8 +10276,43 @@ function redactBaseUrl(message, baseUrl, host) {
|
|
|
10033
10276
|
return baseUrl ? message.split(baseUrl).join(host) : message;
|
|
10034
10277
|
}
|
|
10035
10278
|
|
|
10036
|
-
|
|
10037
|
-
|
|
10279
|
+
// src/app/alert-outcomes.ts
|
|
10280
|
+
/**
|
|
10281
|
+
* 写入单条告警的处置结果。
|
|
10282
|
+
*
|
|
10283
|
+
* 缺 `upstreamId` 的告警无法与平台 `ScanResult` 对齐(唯一索引依赖它),只 warn 不写入。
|
|
10284
|
+
*/
|
|
10285
|
+
function record(sink, alert, status, reason) {
|
|
10286
|
+
const outcomeSink = sink.alertOutcomes;
|
|
10287
|
+
if (!outcomeSink) return;
|
|
10288
|
+
if (!alert.upstreamId) {
|
|
10289
|
+
sink.logger?.warn(`[alert-outcome] ${alert.repository}: ${alert.packageName} has no upstreamId — result not recorded`);
|
|
10290
|
+
return;
|
|
10291
|
+
}
|
|
10292
|
+
outcomeSink.set(alertOutcomeKey(alert), {
|
|
10293
|
+
repository: alert.repository,
|
|
10294
|
+
source: alert.source,
|
|
10295
|
+
upstreamId: alert.upstreamId,
|
|
10296
|
+
manifestPath: alert.manifestPath,
|
|
10297
|
+
status,
|
|
10298
|
+
reason
|
|
10299
|
+
});
|
|
10300
|
+
}
|
|
10301
|
+
/**
|
|
10302
|
+
* 记录「已收敛」告警。
|
|
10303
|
+
*
|
|
10304
|
+
* 调用方必须用**同一数组**同时累加 `RunSummary.alertsConverged` 与调用本函数,
|
|
10305
|
+
* 才能保证明细与计数逐条对齐(平台据此剔除「未修复」明细)。
|
|
10306
|
+
*/
|
|
10307
|
+
function recordConvergedAlerts(sink, alerts, reason) {
|
|
10308
|
+
for (const alert of alerts) record(sink, alert, "converged", reason);
|
|
10309
|
+
}
|
|
10310
|
+
/** 记录「主动跳过」告警;原因取 `SKIP_REASONS` 标识,便于消费方按原因聚合。 */
|
|
10311
|
+
function recordSkippedAlerts(sink, alerts, reason) {
|
|
10312
|
+
for (const alert of alerts) record(sink, alert, "skipped", reason);
|
|
10313
|
+
}
|
|
10314
|
+
|
|
10315
|
+
// src/app/environmental-failure.ts
|
|
10038
10316
|
/**
|
|
10039
10317
|
* 记一条 `ENVIRONMENT_ERROR` 审计条目并返回动作标记。
|
|
10040
10318
|
*
|
|
@@ -10057,8 +10335,7 @@ function markEnvironmentalInstallFailure(ctx, params) {
|
|
|
10057
10335
|
return { environmentalFailure: true };
|
|
10058
10336
|
}
|
|
10059
10337
|
|
|
10060
|
-
|
|
10061
|
-
//#region src/app/family-upgrade.ts
|
|
10338
|
+
// src/app/family-upgrade.ts
|
|
10062
10339
|
/**
|
|
10063
10340
|
* 尝试按家族锁步升级告警对应的直接依赖。
|
|
10064
10341
|
*
|
|
@@ -10248,6 +10525,7 @@ function blockedAttempt(ctx, alert, plan, startedAt, triggerFromVersion) {
|
|
|
10248
10525
|
message: reason
|
|
10249
10526
|
});
|
|
10250
10527
|
addSkipped(ctx.summary, "override-protected");
|
|
10528
|
+
recordSkippedAlerts(ctx, [alert], "override-protected");
|
|
10251
10529
|
return {
|
|
10252
10530
|
status: "blocked",
|
|
10253
10531
|
plan,
|
|
@@ -10328,8 +10606,7 @@ function buildAction(alert, versions, startedAt, outcome, family) {
|
|
|
10328
10606
|
};
|
|
10329
10607
|
}
|
|
10330
10608
|
|
|
10331
|
-
|
|
10332
|
-
//#region src/app/upgrade-alert.ts
|
|
10609
|
+
// src/app/upgrade-alert.ts
|
|
10333
10610
|
/**
|
|
10334
10611
|
* 升级单个告警对应的依赖(dry-run 仅记录)。
|
|
10335
10612
|
* 优先家族锁步(同生态成员同版本线),其次直接升级,失败且为间接依赖时回退到 pnpm overrides。
|
|
@@ -10373,7 +10650,10 @@ async function upgradeAlert(ctx, alert, options = {}) {
|
|
|
10373
10650
|
toVersion: alert.recommendedVersion,
|
|
10374
10651
|
scope: "override"
|
|
10375
10652
|
});
|
|
10376
|
-
if (protectedAction)
|
|
10653
|
+
if (protectedAction) {
|
|
10654
|
+
recordSkippedAlerts(ctx, [alert], "override-protected");
|
|
10655
|
+
return protectedAction;
|
|
10656
|
+
}
|
|
10377
10657
|
strategy = "override";
|
|
10378
10658
|
result = await overrideTransitiveDependency({
|
|
10379
10659
|
packageName: alert.packageName,
|
|
@@ -10439,8 +10719,59 @@ async function upgradeAlert(ctx, alert, options = {}) {
|
|
|
10439
10719
|
}
|
|
10440
10720
|
}
|
|
10441
10721
|
|
|
10442
|
-
|
|
10443
|
-
|
|
10722
|
+
// src/app/budget.ts
|
|
10723
|
+
/**
|
|
10724
|
+
* 启动新步骤所需的最小剩余预算。
|
|
10725
|
+
*
|
|
10726
|
+
* 取 1 分钟:低于该值时,一次安装 / 验证几乎必然被平台超时打断,只会产出
|
|
10727
|
+
* 「执行中被杀」的失败噪声;停止启动新工作更符合「已完成的修复仍交付」的取舍。
|
|
10728
|
+
*/
|
|
10729
|
+
const MIN_STEP_BUDGET_MS = 6e4;
|
|
10730
|
+
/** 剩余预算(毫秒);未设置截止时间时返回 `undefined`。 */
|
|
10731
|
+
function remainingBudgetMs(deadlineAt, now = Date.now()) {
|
|
10732
|
+
if (deadlineAt === void 0) return;
|
|
10733
|
+
return deadlineAt - now;
|
|
10734
|
+
}
|
|
10735
|
+
/** 是否还有足够预算启动下一个步骤(未设置截止时间 → `true`)。 */
|
|
10736
|
+
function hasBudgetForNextStep(deadlineAt, now = Date.now()) {
|
|
10737
|
+
const remaining = remainingBudgetMs(deadlineAt, now);
|
|
10738
|
+
return remaining === void 0 || remaining >= 6e4;
|
|
10739
|
+
}
|
|
10740
|
+
/**
|
|
10741
|
+
* 记录一次「预算不足 → 跳过启动新工作」。
|
|
10742
|
+
*
|
|
10743
|
+
* 审计条目进入 `allErrors`(报告 Errors 表可见),但 `BUDGET_EXHAUSTED` 在跳过类白名单内,
|
|
10744
|
+
* 不影响 exit code / 仓库成功判定。同一 `repository:stage:work` 只记一次(循环里逐包命中不刷屏;
|
|
10745
|
+
* 多仓运行时每个仓库各自留痕)。
|
|
10746
|
+
*/
|
|
10747
|
+
function recordBudgetExhausted(target, input) {
|
|
10748
|
+
const key = `${input.repository}:${input.stage}:${input.work}`;
|
|
10749
|
+
const seen = target.budgetSkips;
|
|
10750
|
+
if (seen?.has(key)) return;
|
|
10751
|
+
seen?.add(key);
|
|
10752
|
+
const remaining = remainingBudgetMs(target.deadlineAt);
|
|
10753
|
+
const remainingText = remaining === void 0 ? "unknown" : `${Math.max(0, remaining)}ms`;
|
|
10754
|
+
target.logger.warn(`[budget] ${input.repository}: run budget exhausted (remaining ${remainingText}, minimum ${MIN_STEP_BUDGET_MS}ms) — skipping ${input.work}`);
|
|
10755
|
+
target.allErrors.push({
|
|
10756
|
+
repository: input.repository,
|
|
10757
|
+
stage: input.stage,
|
|
10758
|
+
category: "BUDGET_EXHAUSTED",
|
|
10759
|
+
message: `Run budget exhausted before ${input.work} (remaining ${remainingText}); no new work was started. Already-verified units are still delivered.`
|
|
10760
|
+
});
|
|
10761
|
+
}
|
|
10762
|
+
/**
|
|
10763
|
+
* 预算检查 + 留痕的组合入口:返回 `false` 表示调用方应停止启动新工作。
|
|
10764
|
+
*
|
|
10765
|
+
* 典型用法(每包 / 每组 / 每次验证前):
|
|
10766
|
+
* `if (!budgetAllows(ctx, { repository: repo, stage: 'fix', work: 'package upgrade' })) break`。
|
|
10767
|
+
*/
|
|
10768
|
+
function budgetAllows(target, input, now = Date.now()) {
|
|
10769
|
+
if (hasBudgetForNextStep(target.deadlineAt, now)) return true;
|
|
10770
|
+
recordBudgetExhausted(target, input);
|
|
10771
|
+
return false;
|
|
10772
|
+
}
|
|
10773
|
+
|
|
10774
|
+
// src/app/verification-checkpoint.ts
|
|
10444
10775
|
/** 当前仓库最近一个 checkpoint(按记录顺序)。 */
|
|
10445
10776
|
const findLatestVerificationCheckpoint = (ctx, repository) => {
|
|
10446
10777
|
const checkpoints = ctx.verificationCheckpoints ?? [];
|
|
@@ -10451,32 +10782,25 @@ const findLatestVerificationCheckpoint = (ctx, repository) => {
|
|
|
10451
10782
|
* `git stash create` 只捕获已跟踪文件;未跟踪文件(如 AI 新建源码)不在本批范围。
|
|
10452
10783
|
* 工作区干净时回退到 HEAD,保证首个单元也有可回退基线。
|
|
10453
10784
|
*/
|
|
10454
|
-
const recordVerificationCheckpoint = (ctx, repo, progress, label) => {
|
|
10785
|
+
const recordVerificationCheckpoint = async (ctx, repo, progress, label) => {
|
|
10455
10786
|
if (ctx.config.dryRun || (ctx.config.rollbackGranularity ?? "unit") !== "unit" || !ctx.verificationCheckpoints) return;
|
|
10456
10787
|
if (!isGitRepository(ctx.workDir)) return;
|
|
10457
10788
|
try {
|
|
10458
|
-
|
|
10789
|
+
const stash = await runGit([
|
|
10459
10790
|
"stash",
|
|
10460
10791
|
"create",
|
|
10461
10792
|
`dependfix:${repo}:${label}`
|
|
10462
|
-
], {
|
|
10463
|
-
|
|
10464
|
-
|
|
10465
|
-
|
|
10466
|
-
|
|
10467
|
-
|
|
10468
|
-
|
|
10469
|
-
|
|
10470
|
-
}).trim();
|
|
10471
|
-
if (!id) id = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
10793
|
+
], { cwd: ctx.workDir });
|
|
10794
|
+
if (!stash.started) return;
|
|
10795
|
+
let id = stash.stdout.trim();
|
|
10796
|
+
if (!id && stash.code !== 0) {
|
|
10797
|
+
ctx.logger.warn(`[rollback] failed to create verification checkpoint for ${repo} (${label}): git stash create exited with code ${stash.code}${stash.stderr.trim() ? ` — ${stash.stderr.trim().slice(-200)}` : ""}`);
|
|
10798
|
+
return;
|
|
10799
|
+
}
|
|
10800
|
+
if (!id) id = (await runGitOrThrow(["rev-parse", "HEAD"], {
|
|
10472
10801
|
cwd: ctx.workDir,
|
|
10473
|
-
|
|
10474
|
-
|
|
10475
|
-
"ignore",
|
|
10476
|
-
"pipe",
|
|
10477
|
-
"pipe"
|
|
10478
|
-
]
|
|
10479
|
-
}).trim();
|
|
10802
|
+
what: "resolve checkpoint HEAD"
|
|
10803
|
+
})).trim();
|
|
10480
10804
|
if (!id) return;
|
|
10481
10805
|
const checkpoint = {
|
|
10482
10806
|
id,
|
|
@@ -10499,15 +10823,15 @@ const recordVerificationCheckpoint = (ctx, repo, progress, label) => {
|
|
|
10499
10823
|
}
|
|
10500
10824
|
};
|
|
10501
10825
|
/** 恢复 checkpoint:工作树 / 暂存区回到 checkpoint,HEAD 不变。 */
|
|
10502
|
-
const restoreCheckpoint = (ctx, checkpoint) => {
|
|
10503
|
-
|
|
10826
|
+
const restoreCheckpoint = async (ctx, checkpoint) => {
|
|
10827
|
+
await runGitOrThrow([
|
|
10504
10828
|
"read-tree",
|
|
10505
10829
|
"--reset",
|
|
10506
10830
|
"-u",
|
|
10507
10831
|
checkpoint.id
|
|
10508
10832
|
], {
|
|
10509
10833
|
cwd: ctx.workDir,
|
|
10510
|
-
|
|
10834
|
+
what: "restore verification checkpoint"
|
|
10511
10835
|
});
|
|
10512
10836
|
};
|
|
10513
10837
|
const restoreProgressAndActions = (ctx, progress, checkpoint) => {
|
|
@@ -10563,7 +10887,7 @@ const attemptSelectiveUnitRollback = async (ctx, repo, progress, verify, options
|
|
|
10563
10887
|
};
|
|
10564
10888
|
}
|
|
10565
10889
|
try {
|
|
10566
|
-
restoreCheckpoint(ctx, checkpoint);
|
|
10890
|
+
await restoreCheckpoint(ctx, checkpoint);
|
|
10567
10891
|
restoreProgressAndActions(ctx, progress, checkpoint);
|
|
10568
10892
|
} catch (error) {
|
|
10569
10893
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -10607,8 +10931,7 @@ const attemptSelectiveUnitRollback = async (ctx, repo, progress, verify, options
|
|
|
10607
10931
|
};
|
|
10608
10932
|
};
|
|
10609
10933
|
|
|
10610
|
-
|
|
10611
|
-
//#region src/app/upgrade-failure.ts
|
|
10934
|
+
// src/app/upgrade-failure.ts
|
|
10612
10935
|
/**
|
|
10613
10936
|
* 记一次升级失败:失败计数自增 + 日志留痕。
|
|
10614
10937
|
*
|
|
@@ -10623,8 +10946,7 @@ function recordUpgradeFailure(ctx, counter, scope, name, error) {
|
|
|
10623
10946
|
ctx.logger.warn(`[${scope}] ${name}: upgrade failed — ${error || "no error detail"}`);
|
|
10624
10947
|
}
|
|
10625
10948
|
|
|
10626
|
-
|
|
10627
|
-
//#region src/app/repo-fix-member.ts
|
|
10949
|
+
// src/app/repo-fix-member.ts
|
|
10628
10950
|
/**
|
|
10629
10951
|
* 步骤 3:成员级升级(workspace 成员 manifest 直接依赖;member 桶准入已保证:
|
|
10630
10952
|
* 成员白名单 + 直接声明 + fixable + 单版本 + 非跨线)。按「包名 + manifestDir」
|
|
@@ -10657,6 +10979,11 @@ async function applyMemberUpgrades(ctx, repo, sets, progress) {
|
|
|
10657
10979
|
}
|
|
10658
10980
|
for (const item of [...memberByPackageAndDir.values()]) {
|
|
10659
10981
|
const { alert, manifestDir } = item;
|
|
10982
|
+
if (!budgetAllows(ctx, {
|
|
10983
|
+
repository: repo,
|
|
10984
|
+
stage: "fix",
|
|
10985
|
+
work: `member upgrade ${alert.packageName}`
|
|
10986
|
+
})) return false;
|
|
10660
10987
|
const memberManifestPath = `${manifestDir}/package.json`;
|
|
10661
10988
|
if (ctx.config.dryRun) {
|
|
10662
10989
|
ctx.logger.info(`[dry-run] Would upgrade ${alert.packageName} in ${memberManifestPath} → ${alert.recommendedVersion}`);
|
|
@@ -10676,7 +11003,7 @@ async function applyMemberUpgrades(ctx, repo, sets, progress) {
|
|
|
10676
11003
|
continue;
|
|
10677
11004
|
}
|
|
10678
11005
|
const memberSnapshot = snapshotTrackedFiles(ctx.workDir, [memberManifestPath]);
|
|
10679
|
-
recordVerificationCheckpoint(ctx, repo, progress, `member-upgrade:${alert.packageName}`);
|
|
11006
|
+
await recordVerificationCheckpoint(ctx, repo, progress, `member-upgrade:${alert.packageName}`);
|
|
10680
11007
|
ctx.logger.warn(`[member-upgrade] ${alert.packageName}: upgrading member declaration in ${memberManifestPath} → ${alert.recommendedVersion}`);
|
|
10681
11008
|
const familyAttempt = await attemptFamilyUpgrade(ctx, {
|
|
10682
11009
|
alert,
|
|
@@ -10783,8 +11110,7 @@ async function applyMemberUpgrades(ctx, repo, sets, progress) {
|
|
|
10783
11110
|
return false;
|
|
10784
11111
|
}
|
|
10785
11112
|
|
|
10786
|
-
|
|
10787
|
-
//#region src/app/repo-fix-batch.ts
|
|
11113
|
+
// src/app/repo-fix-batch.ts
|
|
10788
11114
|
/**
|
|
10789
11115
|
* 尝试合批应用版本化 overrides(调用方仅在条目数 ≥ 2 时调用)。
|
|
10790
11116
|
*
|
|
@@ -10793,9 +11119,18 @@ async function applyMemberUpgrades(ctx, repo, sets, progress) {
|
|
|
10793
11119
|
* `skipSequential: true` = 环境性失败(信号 / 超时 / 无 stderr 诊断),逐包重试无归因价值且会耗尽预算
|
|
10794
11120
|
*/
|
|
10795
11121
|
async function tryBatchVersionedOverrides(ctx, repo, progress, items) {
|
|
11122
|
+
if (!budgetAllows(ctx, {
|
|
11123
|
+
repository: repo,
|
|
11124
|
+
stage: "fix",
|
|
11125
|
+
work: `batched versioned overrides (${items.length} packages)`
|
|
11126
|
+
})) return {
|
|
11127
|
+
applied: false,
|
|
11128
|
+
budgetStop: true,
|
|
11129
|
+
reason: "run budget exhausted"
|
|
11130
|
+
};
|
|
10796
11131
|
const snapshot = snapshotTrackedFiles(ctx.workDir);
|
|
10797
11132
|
const packages = items.map((item) => item.alert.packageName);
|
|
10798
|
-
recordVerificationCheckpoint(ctx, repo, progress, `versioned-override-batch:${packages.join(",")}`);
|
|
11133
|
+
await recordVerificationCheckpoint(ctx, repo, progress, `versioned-override-batch:${packages.join(",")}`);
|
|
10799
11134
|
ctx.logger.info(`[multi-version] 合批 ${items.length} 个包:${packages.join(", ")}(一次安装 + 组级验证)`);
|
|
10800
11135
|
const result = await applyVersionedOverridesBatch({
|
|
10801
11136
|
workDir: ctx.workDir,
|
|
@@ -10855,8 +11190,7 @@ async function tryBatchVersionedOverrides(ctx, repo, progress, items) {
|
|
|
10855
11190
|
return { applied: true };
|
|
10856
11191
|
}
|
|
10857
11192
|
|
|
10858
|
-
|
|
10859
|
-
//#region src/app/verify-project.ts
|
|
11193
|
+
// src/app/verify-project.ts
|
|
10860
11194
|
/** 日志中的外部可控文本(YAML 解析原因等)单行夹取,避免多行片段污染日志。 */
|
|
10861
11195
|
function clipLogText(text, maxLength = 200) {
|
|
10862
11196
|
const safe = text.replace(/\s+/g, " ").trim();
|
|
@@ -11045,6 +11379,14 @@ async function verifyProject(ctx, repo, options = {}) {
|
|
|
11045
11379
|
logger.info(`No verification commands to run for ${repo}`);
|
|
11046
11380
|
return [];
|
|
11047
11381
|
}
|
|
11382
|
+
if (!budgetAllows(ctx, {
|
|
11383
|
+
repository: repo,
|
|
11384
|
+
stage: "verify",
|
|
11385
|
+
work: "post-fix verification"
|
|
11386
|
+
})) {
|
|
11387
|
+
options.onSkip?.("budget");
|
|
11388
|
+
return [];
|
|
11389
|
+
}
|
|
11048
11390
|
const baselineFailedCommands = new Set((options.baseline ?? []).filter((cr) => cr.exitCode !== 0).map((cr) => cr.command));
|
|
11049
11391
|
try {
|
|
11050
11392
|
const result = await runVerification({
|
|
@@ -11109,8 +11451,7 @@ async function verifyProject(ctx, repo, options = {}) {
|
|
|
11109
11451
|
}
|
|
11110
11452
|
}
|
|
11111
11453
|
|
|
11112
|
-
|
|
11113
|
-
//#region src/app/repo-fix-finalize.ts
|
|
11454
|
+
// src/app/repo-fix-finalize.ts
|
|
11114
11455
|
/**
|
|
11115
11456
|
* 惰性取得本次运行修复前的验证基线(每运行一次),并返回本仓库的归因豁免判定。
|
|
11116
11457
|
* dry-run 不做验证,因此也不需要基线。
|
|
@@ -11123,6 +11464,11 @@ async function verifyProject(ctx, repo, options = {}) {
|
|
|
11123
11464
|
*/
|
|
11124
11465
|
async function ensureVerificationBaseline(ctx, repo, options) {
|
|
11125
11466
|
if (ctx.config.dryRun || !ctx.verificationBaseline) return { exemptAttribution: false };
|
|
11467
|
+
if (!budgetAllows(ctx, {
|
|
11468
|
+
repository: repo,
|
|
11469
|
+
stage: "verify",
|
|
11470
|
+
work: "pre-fix baseline sampling"
|
|
11471
|
+
})) return { exemptAttribution: false };
|
|
11126
11472
|
if (ctx.verificationBaseline.value) return {
|
|
11127
11473
|
exemptAttribution: false,
|
|
11128
11474
|
baseline: ctx.verificationBaseline.repo === repo ? ctx.verificationBaseline.value : void 0
|
|
@@ -11174,7 +11520,11 @@ function recordInstallBaselineFailure(ctx, repo, baseline) {
|
|
|
11174
11520
|
*/
|
|
11175
11521
|
async function finalizeRepoFix(ctx, repo, progress, options = {}) {
|
|
11176
11522
|
let lockfileRepairFailed = false;
|
|
11177
|
-
if (progress.alertsCount > 0 && options.exemptAttribution !== true
|
|
11523
|
+
if (progress.alertsCount > 0 && options.exemptAttribution !== true && budgetAllows(ctx, {
|
|
11524
|
+
repository: repo,
|
|
11525
|
+
stage: "repair",
|
|
11526
|
+
work: "lockfile repair"
|
|
11527
|
+
})) {
|
|
11178
11528
|
const repairAction = await tryLockfileRepair(ctx, repo);
|
|
11179
11529
|
ctx.allActions.push(repairAction);
|
|
11180
11530
|
lockfileRepairFailed = !repairAction.success && !repairAction.noOp;
|
|
@@ -11183,32 +11533,50 @@ async function finalizeRepoFix(ctx, repo, progress, options = {}) {
|
|
|
11183
11533
|
else ctx.logger.info(`[skip] No effective work for ${repo} — skipping lockfile repair (workspace stays pristine for attribution)`);
|
|
11184
11534
|
const lockfileRepairChanged = progress.lockfileRepaired;
|
|
11185
11535
|
if (!ctx.config.dryRun) {
|
|
11536
|
+
let budgetSkipped = false;
|
|
11186
11537
|
const verifyOptions = {
|
|
11187
11538
|
baseline: currentBaseline(ctx),
|
|
11188
11539
|
noChanges: options.exemptAttribution === true,
|
|
11189
11540
|
ciAlignment: {
|
|
11190
11541
|
client: options.client ?? null,
|
|
11191
11542
|
defaultBranch: progress.defaultBranch
|
|
11543
|
+
},
|
|
11544
|
+
onSkip: (reason) => {
|
|
11545
|
+
if (reason === "budget") budgetSkipped = true;
|
|
11192
11546
|
}
|
|
11193
11547
|
};
|
|
11194
11548
|
let verifyActions = await verifyProject(ctx, repo, verifyOptions);
|
|
11195
11549
|
let blocking = verifyActions.some((a) => !a.success && !a.preExisting);
|
|
11196
|
-
|
|
11550
|
+
const firstVerificationSkipped = budgetSkipped;
|
|
11551
|
+
if (!firstVerificationSkipped && blocking && (ctx.config.rollbackGranularity ?? "unit") === "unit" && !options.exemptAttribution && !ctx.preExistingDirty && !lockfileRepairFailed) {
|
|
11197
11552
|
const outcome = await attemptSelectiveUnitRollback(ctx, repo, progress, () => verifyProject(ctx, repo, verifyOptions), { beforeVerify: async () => {
|
|
11198
|
-
if (lockfileRepairChanged && progress.alertsCount > 0
|
|
11553
|
+
if (lockfileRepairChanged && progress.alertsCount > 0 && budgetAllows(ctx, {
|
|
11554
|
+
repository: repo,
|
|
11555
|
+
stage: "repair",
|
|
11556
|
+
work: "lockfile repair after selective rollback"
|
|
11557
|
+
})) {
|
|
11199
11558
|
const repairAction = await tryLockfileRepair(ctx, repo);
|
|
11200
11559
|
ctx.allActions.push(repairAction);
|
|
11201
11560
|
if (repairAction.success && !repairAction.noOp) progress.lockfileRepaired = true;
|
|
11202
11561
|
}
|
|
11203
11562
|
} });
|
|
11204
11563
|
if (outcome.applied) {
|
|
11205
|
-
|
|
11206
|
-
|
|
11564
|
+
if (budgetSkipped) blocking = true;
|
|
11565
|
+
else {
|
|
11566
|
+
verifyActions = outcome.verifyActions;
|
|
11567
|
+
blocking = outcome.blocking;
|
|
11568
|
+
}
|
|
11207
11569
|
}
|
|
11208
11570
|
}
|
|
11209
11571
|
ctx.allActions.push(...verifyActions);
|
|
11210
|
-
|
|
11211
|
-
|
|
11572
|
+
if (firstVerificationSkipped) {
|
|
11573
|
+
progress.verificationPassed = void 0;
|
|
11574
|
+
progress.verificationBlocking = true;
|
|
11575
|
+
progress.verificationSkipped = "budget";
|
|
11576
|
+
} else {
|
|
11577
|
+
progress.verificationPassed = verifyActions.every((a) => a.success);
|
|
11578
|
+
progress.verificationBlocking = blocking;
|
|
11579
|
+
}
|
|
11212
11580
|
} else {
|
|
11213
11581
|
ctx.logger.info(`[dry-run] Skipping verification for ${repo}`);
|
|
11214
11582
|
progress.verificationPassed = void 0;
|
|
@@ -11216,8 +11584,7 @@ async function finalizeRepoFix(ctx, repo, progress, options = {}) {
|
|
|
11216
11584
|
}
|
|
11217
11585
|
}
|
|
11218
11586
|
|
|
11219
|
-
|
|
11220
|
-
//#region src/ai/changelog-fetcher.ts
|
|
11587
|
+
// src/ai/changelog-fetcher.ts
|
|
11221
11588
|
/** breaking 段落标题启发式(标题行命中任一关键词即进入收集模式;词边界防子串误匹配) */
|
|
11222
11589
|
const BREAKING_HEADING_RE = /\bbreaking\s*changes?\b|\bbreaking-changes\b|\bmigration\b|⚠️|🚨/i;
|
|
11223
11590
|
/** 收集模式的列表项行(`- ` / `* ` / `+ ` / `1. `,允许缩进) */
|
|
@@ -11370,8 +11737,7 @@ function extractBreakingChanges(markdown) {
|
|
|
11370
11737
|
return entries;
|
|
11371
11738
|
}
|
|
11372
11739
|
|
|
11373
|
-
|
|
11374
|
-
//#region src/ai/patch-applier.ts
|
|
11740
|
+
// src/ai/patch-applier.ts
|
|
11375
11741
|
/**
|
|
11376
11742
|
* 应用 AI 结构化修改到工作区。
|
|
11377
11743
|
*
|
|
@@ -11540,8 +11906,7 @@ function failure(error, appliedFiles, rollback) {
|
|
|
11540
11906
|
};
|
|
11541
11907
|
}
|
|
11542
11908
|
|
|
11543
|
-
|
|
11544
|
-
//#region src/ai/safety-gate.ts
|
|
11909
|
+
// src/ai/safety-gate.ts
|
|
11545
11910
|
const DEFAULT_MAX_FILES = 5;
|
|
11546
11911
|
/** 敏感信息模式(凭据/token/私钥)——命中即拒绝(防泄露进 PR/报告) */
|
|
11547
11912
|
const SECRET_PATTERNS = [
|
|
@@ -11615,8 +11980,7 @@ function validateAiChanges(workDir, changes, options = {}) {
|
|
|
11615
11980
|
};
|
|
11616
11981
|
}
|
|
11617
11982
|
|
|
11618
|
-
|
|
11619
|
-
//#region src/ai/app-integration.ts
|
|
11983
|
+
// src/ai/app-integration.ts
|
|
11620
11984
|
/**
|
|
11621
11985
|
* 执行一次 AI 研判集成(changelog 采集 → 研判 → 分流)。
|
|
11622
11986
|
*
|
|
@@ -11776,8 +12140,7 @@ function logUsage(logger, packageName, model, usage) {
|
|
|
11776
12140
|
logger.info(`[ai] ${packageName} 研判消耗: ${usage.calls} 次调用, ${usage.inputTokens} in / ${usage.outputTokens} out tokens${costText} (${model})`);
|
|
11777
12141
|
}
|
|
11778
12142
|
|
|
11779
|
-
|
|
11780
|
-
//#region src/app/repo-fix-major.ts
|
|
12143
|
+
// src/app/repo-fix-major.ts
|
|
11781
12144
|
async function applyAutoMajorUpgrades(ctx, params) {
|
|
11782
12145
|
const { client, repo, lockfilePath, alerts: autoMajorAlerts, progress } = params;
|
|
11783
12146
|
let stopReason;
|
|
@@ -11793,6 +12156,11 @@ async function applyAutoMajorUpgrades(ctx, params) {
|
|
|
11793
12156
|
const majorFamilyTargets = {};
|
|
11794
12157
|
for (const alert of autoMajorRepresentatives) if (alert.recommendedVersion) majorFamilyTargets[alert.packageName] = alert.recommendedVersion;
|
|
11795
12158
|
for (const alert of autoMajorRepresentatives) {
|
|
12159
|
+
if (!budgetAllows(ctx, {
|
|
12160
|
+
repository: repo,
|
|
12161
|
+
stage: "fix",
|
|
12162
|
+
work: `major upgrade ${alert.packageName}`
|
|
12163
|
+
})) return;
|
|
11796
12164
|
if (ctx.config.dryRun) {
|
|
11797
12165
|
ctx.logger.info(`[dry-run] Would apply major upgrade for ${alert.packageName} → ${alert.recommendedVersion}`);
|
|
11798
12166
|
ctx.allActions.push({
|
|
@@ -11810,7 +12178,7 @@ async function applyAutoMajorUpgrades(ctx, params) {
|
|
|
11810
12178
|
continue;
|
|
11811
12179
|
}
|
|
11812
12180
|
const majorSnapshot = snapshotTrackedFiles(ctx.workDir);
|
|
11813
|
-
recordVerificationCheckpoint(ctx, repo, progress, `major-upgrade:${alert.packageName}`);
|
|
12181
|
+
await recordVerificationCheckpoint(ctx, repo, progress, `major-upgrade:${alert.packageName}`);
|
|
11814
12182
|
ctx.logger.warn(`[major-upgrade] ${alert.packageName}: applying cross-major upgrade → ${alert.recommendedVersion} (explicit --allow-major-upgrade; full verification required)`);
|
|
11815
12183
|
const familyAttempt = await attemptFamilyUpgrade(ctx, {
|
|
11816
12184
|
alert,
|
|
@@ -11941,8 +12309,7 @@ async function applyAutoMajorUpgrades(ctx, params) {
|
|
|
11941
12309
|
return stopReason;
|
|
11942
12310
|
}
|
|
11943
12311
|
|
|
11944
|
-
|
|
11945
|
-
//#region src/app/repeated-failure.ts
|
|
12312
|
+
// src/app/repeated-failure.ts
|
|
11946
12313
|
/** 同键允许的已关闭尝试次数:第 1 / 2 次执行,第 3 次起跳过。 */
|
|
11947
12314
|
const REPEATED_FAILURE_ATTEMPT_LIMIT = 2;
|
|
11948
12315
|
/**
|
|
@@ -12001,6 +12368,7 @@ async function applyRepeatedFailureSkips(options) {
|
|
|
12001
12368
|
skippedIds.add(alert.id);
|
|
12002
12369
|
ctx.allActions.push(createSkippedAction(alert));
|
|
12003
12370
|
addSkipped(ctx.summary, "repeated-failure");
|
|
12371
|
+
recordSkippedAlerts(ctx, [alert], "repeated-failure");
|
|
12004
12372
|
ctx.logger.warn(`[repeated-failure] ${alert.repository}: ${alert.packageName} → ${alert.recommendedVersion} already failed ${attempt - 1} time(s) (attempt ${attempt}/${2}) — skipped; use --retry-failed to force another attempt`);
|
|
12005
12373
|
} else if (forced) ctx.logger.warn(`[repeated-failure] ${alert.repository}: ${alert.packageName} → ${alert.recommendedVersion} attempt ${attempt}/${2} exceeded — forced retry (--retry-failed)`);
|
|
12006
12374
|
ctx.attemptTracking?.push({
|
|
@@ -12045,8 +12413,7 @@ function createSkippedAction(alert) {
|
|
|
12045
12413
|
};
|
|
12046
12414
|
}
|
|
12047
12415
|
|
|
12048
|
-
|
|
12049
|
-
//#region src/app/repo-fix.ts
|
|
12416
|
+
// src/app/repo-fix.ts
|
|
12050
12417
|
/**
|
|
12051
12418
|
* 单仓库修复管线编排器(原 processRepoForFix):
|
|
12052
12419
|
* 抓取 → 模板修复 → lockfile 脆弱实例(版本化 overrides / 跨线 / 成员)→
|
|
@@ -12103,6 +12470,7 @@ async function processRepoFix(ctx, client, repo) {
|
|
|
12103
12470
|
lockfileRepaired: progress.lockfileRepaired,
|
|
12104
12471
|
verificationPassed: progress.verificationPassed,
|
|
12105
12472
|
verificationBlocking: progress.verificationBlocking,
|
|
12473
|
+
verificationSkipped: progress.verificationSkipped,
|
|
12106
12474
|
durationMs: Date.now() - startTime
|
|
12107
12475
|
});
|
|
12108
12476
|
}
|
|
@@ -12170,9 +12538,11 @@ async function prepareRepoFixSets(ctx, repo, planned, limited, progress) {
|
|
|
12170
12538
|
if (submanifestAlerts.length > 0) {
|
|
12171
12539
|
ctx.logger.warn(`[alerts] ${submanifestAlerts.length} alert(s) from sub-directory / root-direct-dep manifest(s) skipped — manual review required: ${submanifestAlerts.map((a) => `${a.packageName} (${a.manifestPath})`).join(", ")}`);
|
|
12172
12540
|
addSkipped(ctx.summary, "subdirectory-manifest", submanifestAlerts.length);
|
|
12541
|
+
recordSkippedAlerts(ctx, submanifestAlerts, "subdirectory-manifest");
|
|
12173
12542
|
}
|
|
12174
|
-
const
|
|
12175
|
-
addSkipped(ctx.summary, "not-fixable",
|
|
12543
|
+
const notFixableAlerts = rootManifestAlerts.filter((alert) => !alert.fixable || !alert.recommendedVersion);
|
|
12544
|
+
addSkipped(ctx.summary, "not-fixable", notFixableAlerts.length);
|
|
12545
|
+
recordSkippedAlerts(ctx, notFixableAlerts.filter((alert) => !alert.fixable), "not-fixable");
|
|
12176
12546
|
return {
|
|
12177
12547
|
rootManifestAlerts,
|
|
12178
12548
|
memberManifestAlerts,
|
|
@@ -12197,6 +12567,7 @@ async function applyLockfileFixes(ctx, client, repo, sets, progress) {
|
|
|
12197
12567
|
if (manualCrossMajorAlerts.length > 0) {
|
|
12198
12568
|
ctx.logger.warn(`[alerts] ${manualCrossMajorAlerts.length} alert(s) require a cross-major upgrade (no fix within the installed major line) — manual review required: ${manualCrossMajorAlerts.map((a) => `${a.packageName} → ${a.recommendedVersion}`).join(", ")}`);
|
|
12199
12569
|
addSkipped(ctx.summary, "cross-major", manualCrossMajorAlerts.length);
|
|
12570
|
+
recordSkippedAlerts(ctx, manualCrossMajorAlerts, "cross-major");
|
|
12200
12571
|
}
|
|
12201
12572
|
const autoMajorAlerts = lockfileManifestAlerts.filter((a) => autoMajorAlertIds.has(a.id));
|
|
12202
12573
|
const fixableLockfileAlerts = lockfileManifestAlerts.filter((a) => !crossMajorAlertIds.has(a.id) && !autoMajorAlertIds.has(a.id));
|
|
@@ -12224,6 +12595,7 @@ async function applyLockfileFixes(ctx, client, repo, sets, progress) {
|
|
|
12224
12595
|
});
|
|
12225
12596
|
if (protectedAction) {
|
|
12226
12597
|
ctx.allActions.push(protectedAction);
|
|
12598
|
+
recordSkippedAlerts(ctx, [alert], "override-protected");
|
|
12227
12599
|
continue;
|
|
12228
12600
|
}
|
|
12229
12601
|
const versionedOverrides = versionedOverridesByPackage.get(alert.packageName) ?? {};
|
|
@@ -12247,7 +12619,9 @@ async function applyLockfileFixes(ctx, client, repo, sets, progress) {
|
|
|
12247
12619
|
}
|
|
12248
12620
|
if (Object.keys(versionedOverrides).length === 0) {
|
|
12249
12621
|
ctx.logger.info(`Skipping ${alert.packageName}: no vulnerable instances below targets`);
|
|
12250
|
-
|
|
12622
|
+
const convergedAlerts = multiVersionAlerts.filter((a) => a.packageName === alert.packageName);
|
|
12623
|
+
ctx.summary.alertsConverged += convergedAlerts.length;
|
|
12624
|
+
recordConvergedAlerts(ctx, convergedAlerts, "no vulnerable instances below targets");
|
|
12251
12625
|
continue;
|
|
12252
12626
|
}
|
|
12253
12627
|
batchItems.push({
|
|
@@ -12258,6 +12632,13 @@ async function applyLockfileFixes(ctx, client, repo, sets, progress) {
|
|
|
12258
12632
|
}
|
|
12259
12633
|
const batchOutcome = batchItems.length > 1 ? await tryBatchVersionedOverrides(ctx, repo, progress, batchItems) : { applied: false };
|
|
12260
12634
|
const itemsToRetry = batchOutcome.applied || batchOutcome.skipSequential ? [] : batchItems;
|
|
12635
|
+
if (batchOutcome.budgetStop) {
|
|
12636
|
+
stopReason = "budget";
|
|
12637
|
+
return {
|
|
12638
|
+
singleVersionAlerts,
|
|
12639
|
+
stopReason
|
|
12640
|
+
};
|
|
12641
|
+
}
|
|
12261
12642
|
if (batchOutcome.skipSequential) {
|
|
12262
12643
|
stopReason = isAbortRequested() ? "aborted" : "environmental";
|
|
12263
12644
|
return {
|
|
@@ -12270,8 +12651,16 @@ async function applyLockfileFixes(ctx, client, repo, sets, progress) {
|
|
|
12270
12651
|
stopReason = "aborted";
|
|
12271
12652
|
break;
|
|
12272
12653
|
}
|
|
12654
|
+
if (!budgetAllows(ctx, {
|
|
12655
|
+
repository: repo,
|
|
12656
|
+
stage: "fix",
|
|
12657
|
+
work: `versioned override for ${alert.packageName}`
|
|
12658
|
+
})) {
|
|
12659
|
+
stopReason = "budget";
|
|
12660
|
+
break;
|
|
12661
|
+
}
|
|
12273
12662
|
const snapshot = snapshotTrackedFiles(ctx.workDir);
|
|
12274
|
-
recordVerificationCheckpoint(ctx, repo, progress, `versioned-override:${alert.packageName}`);
|
|
12663
|
+
await recordVerificationCheckpoint(ctx, repo, progress, `versioned-override:${alert.packageName}`);
|
|
12275
12664
|
ctx.logger.info(`[multi-version] ${alert.packageName}: applying versioned overrides`, { overrides: versionedOverrides });
|
|
12276
12665
|
const result = await applyVersionedOverrides({
|
|
12277
12666
|
packageName: alert.packageName,
|
|
@@ -12358,12 +12747,22 @@ async function applyGroupUpgrades(ctx, repo, sets, singleVersionAlerts, progress
|
|
|
12358
12747
|
for (const alert of fixableAlerts) if (alert.recommendedVersion) familyTargets[alert.packageName] = alert.recommendedVersion;
|
|
12359
12748
|
let snapshot;
|
|
12360
12749
|
for (const group of groups) {
|
|
12750
|
+
if (!budgetAllows(ctx, {
|
|
12751
|
+
repository: repo,
|
|
12752
|
+
stage: "fix",
|
|
12753
|
+
work: `group upgrade "${group.name}"`
|
|
12754
|
+
})) return;
|
|
12361
12755
|
snapshot = snapshotTrackedFiles(ctx.workDir);
|
|
12362
|
-
recordVerificationCheckpoint(ctx, repo, progress, `group:${group.name}`);
|
|
12756
|
+
await recordVerificationCheckpoint(ctx, repo, progress, `group:${group.name}`);
|
|
12363
12757
|
const pendingActions = [];
|
|
12364
12758
|
const upgradedInGroup = [];
|
|
12365
12759
|
let environmentalStop = false;
|
|
12366
12760
|
for (const packageName of group.packages) {
|
|
12761
|
+
if (!budgetAllows(ctx, {
|
|
12762
|
+
repository: repo,
|
|
12763
|
+
stage: "fix",
|
|
12764
|
+
work: `package upgrade (group "${group.name}")`
|
|
12765
|
+
})) break;
|
|
12367
12766
|
const alert = alertByPackage.get(packageName);
|
|
12368
12767
|
if (!alert) continue;
|
|
12369
12768
|
const lockedVersions = readLockfileVersions(lockfilePath, alert.packageName);
|
|
@@ -12372,11 +12771,14 @@ async function applyGroupUpgrades(ctx, repo, sets, singleVersionAlerts, progress
|
|
|
12372
12771
|
const belowTarget = lockedVersions.filter((version) => compareSemver(version, alert.recommendedVersion) < 0);
|
|
12373
12772
|
if (belowTarget.length === 0) {
|
|
12374
12773
|
ctx.logger.info(`Skipping ${alert.packageName}: all locked versions ${lockedVersions.join(", ")} >= target ${alert.recommendedVersion} (no upgrade needed)`);
|
|
12375
|
-
|
|
12774
|
+
const convergedAlerts = singleVersionAlerts.filter((a) => a.packageName === alert.packageName && a.fixable && a.recommendedVersion);
|
|
12775
|
+
ctx.summary.alertsConverged += convergedAlerts.length;
|
|
12776
|
+
recordConvergedAlerts(ctx, convergedAlerts, "all locked versions >= target");
|
|
12376
12777
|
continue;
|
|
12377
12778
|
}
|
|
12378
12779
|
ctx.logger.warn(`Skipping ${alert.packageName}: locked instance(s) ${belowTarget.join(", ")} remain below target ${alert.recommendedVersion} (lower-major / different manifest instance; cross-major upgrade or manual review required)`);
|
|
12379
12780
|
addSkipped(ctx.summary, "cross-major");
|
|
12781
|
+
recordSkippedAlerts(ctx, singleVersionAlerts.filter((a) => a.packageName === alert.packageName && a.fixable && a.recommendedVersion), "cross-major");
|
|
12380
12782
|
continue;
|
|
12381
12783
|
}
|
|
12382
12784
|
if (lockedVersions.length === 0) ctx.logger.warn(`Could not resolve current version of ${alert.packageName} from lockfile — no-downgrade protection inactive`);
|
|
@@ -12434,7 +12836,12 @@ async function applyGroupUpgrades(ctx, repo, sets, singleVersionAlerts, progress
|
|
|
12434
12836
|
return;
|
|
12435
12837
|
}
|
|
12436
12838
|
for (const alert of upgradedInGroup) {
|
|
12437
|
-
|
|
12839
|
+
if (!budgetAllows(ctx, {
|
|
12840
|
+
repository: repo,
|
|
12841
|
+
stage: "fix",
|
|
12842
|
+
work: `package upgrade ${alert.packageName} (group "${group.name}")`
|
|
12843
|
+
})) return;
|
|
12844
|
+
await recordVerificationCheckpoint(ctx, repo, progress, `group:${group.name}:${alert.packageName}`);
|
|
12438
12845
|
const action = await upgradeAlert(ctx, alert, { familyTargets });
|
|
12439
12846
|
ctx.allActions.push(action);
|
|
12440
12847
|
if (!action.success) {
|
|
@@ -12461,8 +12868,7 @@ async function applyGroupUpgrades(ctx, repo, sets, singleVersionAlerts, progress
|
|
|
12461
12868
|
}
|
|
12462
12869
|
}
|
|
12463
12870
|
|
|
12464
|
-
|
|
12465
|
-
//#region src/app/repo-config.ts
|
|
12871
|
+
// src/app/repo-config.ts
|
|
12466
12872
|
/**
|
|
12467
12873
|
* 目标仓库专属配置路径(相对工作区根)。工作区即目标仓库检出:
|
|
12468
12874
|
* 平台在 clone 之后才构造 `DependfixApp`,CLI 则直接在工作区内运行。
|
|
@@ -12605,8 +13011,7 @@ function applyRepoConfig(config, workDir, logger) {
|
|
|
12605
13011
|
return config;
|
|
12606
13012
|
}
|
|
12607
13013
|
|
|
12608
|
-
|
|
12609
|
-
//#region src/app/git-exclude.ts
|
|
13014
|
+
// src/app/git-exclude.ts
|
|
12610
13015
|
/** 交付提交必须排除的安装产物(目录形态,任意层级)。 */
|
|
12611
13016
|
const GIT_EXCLUDE_ENTRIES = [".pnpm-store/", "node_modules/"];
|
|
12612
13017
|
/** 追加条目时写入的分组注释(便于用户识别来源)。 */
|
|
@@ -12688,8 +13093,7 @@ function prepareDeliveryHygiene(workDir, options, logger) {
|
|
|
12688
13093
|
else if (result.added.length > 0) logger.info(`[git] 已排除安装产物:${result.added.join(", ")}(.git/info/exclude)`);
|
|
12689
13094
|
}
|
|
12690
13095
|
|
|
12691
|
-
|
|
12692
|
-
//#region src/app/pr-check-watch-run.ts
|
|
13096
|
+
// src/app/pr-check-watch-run.ts
|
|
12693
13097
|
/**
|
|
12694
13098
|
* 等待本次创建 PR 的 check 终态并按归因处置。
|
|
12695
13099
|
*
|
|
@@ -12786,10 +13190,9 @@ async function runPrCheckWatch(params) {
|
|
|
12786
13190
|
}
|
|
12787
13191
|
}
|
|
12788
13192
|
|
|
12789
|
-
|
|
12790
|
-
//#region src/app/skipped-delivery.ts
|
|
13193
|
+
// src/app/skipped-delivery.ts
|
|
12791
13194
|
/** 交付因「修复后安装仍既有失败」被跳过时的统一处置(日志 + 审计 + 回滚)。 */
|
|
12792
|
-
function skipDeliveryForInstallFailure(args) {
|
|
13195
|
+
async function skipDeliveryForInstallFailure(args) {
|
|
12793
13196
|
const { logger, workDir, preExistingDirty, allErrors, repos } = args;
|
|
12794
13197
|
const repoList = repos.join(", ");
|
|
12795
13198
|
logger.warn(`Install verification is still failing (pre-existing) after the fix for ${repoList} — skipping delivery; fix the execution environment or target repository lockfile first`);
|
|
@@ -12799,7 +13202,7 @@ function skipDeliveryForInstallFailure(args) {
|
|
|
12799
13202
|
category: "PRE_EXISTING_FAILURE",
|
|
12800
13203
|
message: `Delivery skipped: install verification still failing (pre-existing) for ${repoList}`
|
|
12801
13204
|
});
|
|
12802
|
-
rollbackForSkippedDelivery({
|
|
13205
|
+
await rollbackForSkippedDelivery({
|
|
12803
13206
|
logger,
|
|
12804
13207
|
workDir,
|
|
12805
13208
|
preExistingDirty,
|
|
@@ -12807,56 +13210,124 @@ function skipDeliveryForInstallFailure(args) {
|
|
|
12807
13210
|
});
|
|
12808
13211
|
}
|
|
12809
13212
|
/** 交付被跳过时回滚本次运行产生的改动(非 git / 无改动 / 运行前脏工作区时跳过)。 */
|
|
12810
|
-
function rollbackForSkippedDelivery(args) {
|
|
13213
|
+
async function rollbackForSkippedDelivery(args) {
|
|
12811
13214
|
const { logger, workDir, preExistingDirty, reason } = args;
|
|
12812
13215
|
if (preExistingDirty || !hasGitChanges(workDir)) return;
|
|
12813
13216
|
try {
|
|
12814
|
-
rollbackChanges(workDir);
|
|
13217
|
+
await rollbackChanges(workDir);
|
|
12815
13218
|
logger.info(`Changes rolled back (delivery skipped: ${reason})`);
|
|
12816
13219
|
} catch (error) {
|
|
13220
|
+
if (error instanceof GitCommandError && error.aborted && !error.started) {
|
|
13221
|
+
logger.warn(`Rollback skipped (run aborted before it started): ${reason}`);
|
|
13222
|
+
return;
|
|
13223
|
+
}
|
|
12817
13224
|
logger.warn(`Rollback after skipped delivery failed: ${toErrorMessage(error)}`);
|
|
12818
13225
|
}
|
|
12819
13226
|
}
|
|
12820
|
-
|
|
12821
|
-
//#endregion
|
|
12822
|
-
//#region src/app/duplicate-prs.ts
|
|
12823
13227
|
/**
|
|
12824
|
-
*
|
|
13228
|
+
* `issue` 档:按失败指纹在落点仓库开 issue(同一指纹只开一次,关闭后不重开)。
|
|
12825
13229
|
*
|
|
12826
|
-
*
|
|
13230
|
+
* 载体创建失败只 warn:交付跳过与 exit code 语义不受影响(`PRE_EXISTING_FAILURE` 仍在
|
|
13231
|
+
* 跳过类白名单内)。调用方负责此前的回滚;本函数只登记记录 + 产出载体。
|
|
12827
13232
|
*/
|
|
12828
|
-
async function
|
|
12829
|
-
const {
|
|
12830
|
-
|
|
12831
|
-
|
|
12832
|
-
|
|
12833
|
-
|
|
12834
|
-
|
|
12835
|
-
|
|
12836
|
-
|
|
12837
|
-
""
|
|
12838
|
-
|
|
12839
|
-
].join("\n");
|
|
13233
|
+
async function deliverSkipIssue(params) {
|
|
13234
|
+
const { target, octokit, owner, repo, repositories, failingCommands } = params;
|
|
13235
|
+
const fingerprint = computeDeliverySkipFingerprint({
|
|
13236
|
+
repositories,
|
|
13237
|
+
failingCommands
|
|
13238
|
+
});
|
|
13239
|
+
const record = recordDeliverySkip(target, {
|
|
13240
|
+
repositories: [...repositories],
|
|
13241
|
+
failingCommands: [...failingCommands],
|
|
13242
|
+
carrier: { mode: "issue" }
|
|
13243
|
+
});
|
|
12840
13244
|
try {
|
|
12841
|
-
await
|
|
12842
|
-
|
|
13245
|
+
const existing = await findDeliverySkipIssue(octokit, owner, repo, fingerprint);
|
|
13246
|
+
if (existing) {
|
|
13247
|
+
record.carrier.issue = {
|
|
13248
|
+
...existing,
|
|
13249
|
+
created: false,
|
|
13250
|
+
fingerprint
|
|
13251
|
+
};
|
|
13252
|
+
target.logger.info(`Delivery-skip issue already exists for the same failure fingerprint: ${existing.htmlUrl}`);
|
|
13253
|
+
return record;
|
|
13254
|
+
}
|
|
13255
|
+
const issue = await createDeliverySkipIssue(octokit, owner, repo, {
|
|
13256
|
+
title: buildDeliverySkipIssueTitle(repositories),
|
|
13257
|
+
body: buildDeliverySkipIssueBody({
|
|
13258
|
+
fingerprint,
|
|
13259
|
+
runId: target.runId,
|
|
13260
|
+
repositories,
|
|
13261
|
+
failingCommands
|
|
13262
|
+
}),
|
|
13263
|
+
labels: [DELIVERY_SKIP_LABEL]
|
|
13264
|
+
});
|
|
13265
|
+
record.carrier.issue = {
|
|
13266
|
+
...issue,
|
|
13267
|
+
created: true,
|
|
13268
|
+
fingerprint
|
|
13269
|
+
};
|
|
13270
|
+
target.logger.info(`Delivery-skip issue created: ${issue.htmlUrl}`);
|
|
12843
13271
|
} catch (error) {
|
|
12844
|
-
|
|
12845
|
-
logger.warn(`Failed to add duplicate notice comment to PR #${newPRNumber}: ${message}(token may lack \`issues: write\` scope)`);
|
|
13272
|
+
target.logger.warn(`Delivery-skip issue creation failed (delivery stays skipped, exit code unchanged): ${toErrorMessage(error)}`);
|
|
12846
13273
|
}
|
|
12847
|
-
|
|
12848
|
-
|
|
12849
|
-
|
|
12850
|
-
|
|
12851
|
-
|
|
12852
|
-
|
|
13274
|
+
return record;
|
|
13275
|
+
}
|
|
13276
|
+
/**
|
|
13277
|
+
* 修复后安装仍为既有失败时的交付降级(`DEPENDFIX_ON_BASELINE_RED`)。
|
|
13278
|
+
*
|
|
13279
|
+
* - `skip`(默认):审计记录 + 回滚,返回 `continueAsDraft: false`(行为与引入前逐字一致)
|
|
13280
|
+
* - `issue`:先按「仓库集 + 失败命令集」指纹开 issue(去重、关闭后不重开),随后仍回滚
|
|
13281
|
+
* - `draft`:不回滚,返回 `continueAsDraft: true`——调用方继续 commit / push,PR 以**草稿**创建
|
|
13282
|
+
*
|
|
13283
|
+
* dry-run / 无落点仓库时一律按 `skip` 处理:dry-run 无交付面可降级,且必须回滚。
|
|
13284
|
+
*/
|
|
13285
|
+
async function handleBaselineRedDelivery(args) {
|
|
13286
|
+
const { ctx, config, client, repositories, installBlockedRepos, workDir, preExistingDirty } = args;
|
|
13287
|
+
const mode = config.onBaselineRed ?? "skip";
|
|
13288
|
+
const failingCommands = collectFailingInstallCommands(ctx.allActions ?? [], installBlockedRepos);
|
|
13289
|
+
const firstRepo = repositories[0];
|
|
13290
|
+
const issueRepo = installBlockedRepos[0];
|
|
13291
|
+
if (mode === "draft" && !config.dryRun && firstRepo) {
|
|
13292
|
+
const record = recordDeliverySkip(ctx, {
|
|
13293
|
+
repositories: installBlockedRepos,
|
|
13294
|
+
failingCommands,
|
|
13295
|
+
carrier: { mode: "draft" }
|
|
13296
|
+
});
|
|
13297
|
+
ctx.logger.warn(`Install verification is still failing (pre-existing) after the fix for ${installBlockedRepos.join(", ")} — continuing with a draft PR (DEPENDFIX_ON_BASELINE_RED=draft); CI will fail with the same pre-existing error`);
|
|
13298
|
+
return {
|
|
13299
|
+
continueAsDraft: true,
|
|
13300
|
+
record
|
|
13301
|
+
};
|
|
12853
13302
|
}
|
|
13303
|
+
if (mode === "issue" && !config.dryRun && client && issueRepo) {
|
|
13304
|
+
const [owner, repo] = issueRepo.split("/");
|
|
13305
|
+
await deliverSkipIssue({
|
|
13306
|
+
target: ctx,
|
|
13307
|
+
octokit: client,
|
|
13308
|
+
owner,
|
|
13309
|
+
repo,
|
|
13310
|
+
repositories: installBlockedRepos,
|
|
13311
|
+
failingCommands
|
|
13312
|
+
});
|
|
13313
|
+
} else recordDeliverySkip(ctx, {
|
|
13314
|
+
repositories: installBlockedRepos,
|
|
13315
|
+
failingCommands,
|
|
13316
|
+
carrier: { mode: "skip" }
|
|
13317
|
+
});
|
|
13318
|
+
await skipDeliveryForInstallFailure({
|
|
13319
|
+
logger: ctx.logger,
|
|
13320
|
+
workDir,
|
|
13321
|
+
preExistingDirty,
|
|
13322
|
+
allErrors: ctx.allErrors,
|
|
13323
|
+
repos: installBlockedRepos
|
|
13324
|
+
});
|
|
13325
|
+
return { continueAsDraft: false };
|
|
12854
13326
|
}
|
|
12855
13327
|
|
|
12856
|
-
|
|
12857
|
-
//#region src/app/index.ts
|
|
13328
|
+
// src/app/source-failure-summary.ts
|
|
12858
13329
|
/**
|
|
12859
|
-
*
|
|
13330
|
+
* 输出「部分源拉取失败」汇总(per-source 错误隔离:单源失败不阻断其余源的结果)。
|
|
12860
13331
|
*
|
|
12861
13332
|
* 触发条件(必须全部满足,避免与"仓库失败"(无任何成功源)语义重叠):
|
|
12862
13333
|
* 1. allErrors 至少包含一个 `stage='fetch' + category='FETCH_FAILED'` 错误(per-source 失败)
|
|
@@ -12882,8 +13353,44 @@ function logPartialSourceFailureSummary(allErrors, logger, isAnyRepoSuccessful)
|
|
|
12882
13353
|
});
|
|
12883
13354
|
}
|
|
12884
13355
|
const summary = [...bySource.entries()].map(([source, { repos, sampleMessage }]) => `${source}(${repos.size} 个仓库: ${[...repos].slice(0, 3).join(", ")}${repos.size > 3 ? "..." : ""}): ${sampleMessage}`).join("\n - ");
|
|
12885
|
-
logger.warn(`[alerts] 部分源拉取失败(
|
|
13356
|
+
logger.warn(`[alerts] 部分源拉取失败(per-source 错误隔离)— 成功源已保留继续处理,详细如下:\n - ${summary}`);
|
|
13357
|
+
}
|
|
13358
|
+
|
|
13359
|
+
// src/app/duplicate-prs.ts
|
|
13360
|
+
/**
|
|
13361
|
+
* 重复 PR 处理:在新 PR 上添加评论指向被取代的旧 PR + 添加 `duplicate` label。
|
|
13362
|
+
*
|
|
13363
|
+
* @param logger 调用方 logger(warning 输出;不抛错)
|
|
13364
|
+
*/
|
|
13365
|
+
async function handleDuplicatePRs(params) {
|
|
13366
|
+
const { client, owner, repo, newPRNumber, supersededPRs, logger } = params;
|
|
13367
|
+
if (supersededPRs.length === 0) return;
|
|
13368
|
+
const commentBody = [
|
|
13369
|
+
"## ⚠️ Duplicate PR Notice",
|
|
13370
|
+
"",
|
|
13371
|
+
"This PR supersedes the following dependfix PR(s) with different content:",
|
|
13372
|
+
"",
|
|
13373
|
+
supersededPRs.map((pr) => `- [#${pr.number}](${pr.htmlUrl})`).join("\n"),
|
|
13374
|
+
"",
|
|
13375
|
+
"The superseded PR(s) have been closed. Please review this PR as the new canonical fix."
|
|
13376
|
+
].join("\n");
|
|
13377
|
+
try {
|
|
13378
|
+
await commentOnPullRequest(client, owner, repo, newPRNumber, commentBody);
|
|
13379
|
+
logger.info(`Added duplicate notice comment to PR #${newPRNumber}`);
|
|
13380
|
+
} catch (error) {
|
|
13381
|
+
const message = toErrorMessage(error);
|
|
13382
|
+
logger.warn(`Failed to add duplicate notice comment to PR #${newPRNumber}: ${message}(token may lack \`issues: write\` scope)`);
|
|
13383
|
+
}
|
|
13384
|
+
try {
|
|
13385
|
+
await addLabelToPullRequest(client, owner, repo, newPRNumber, ["duplicate"]);
|
|
13386
|
+
logger.info(`Added 'duplicate' label to PR #${newPRNumber}`);
|
|
13387
|
+
} catch (error) {
|
|
13388
|
+
const message = toErrorMessage(error);
|
|
13389
|
+
logger.warn(`Failed to add 'duplicate' label to PR #${newPRNumber}: ${message}(token may lack \`issues: write\` scope)`);
|
|
13390
|
+
}
|
|
12886
13391
|
}
|
|
13392
|
+
|
|
13393
|
+
// src/app/index.ts
|
|
12887
13394
|
var DependfixApp = class {
|
|
12888
13395
|
/** 运行时配置;构造期按「中央配置优先」合并目标仓库专属配置后单次赋值(见 applyRepoConfig) */
|
|
12889
13396
|
config;
|
|
@@ -12928,6 +13435,13 @@ var DependfixApp = class {
|
|
|
12928
13435
|
attempts: [],
|
|
12929
13436
|
runBudgetExhausted: false
|
|
12930
13437
|
};
|
|
13438
|
+
/** 交付跳过记录(基线红;`computeSummary` 据此派生 summary 计数) */
|
|
13439
|
+
deliverySkips = [];
|
|
13440
|
+
/** 已记录的预算跳过(`stage:work`;逐包命中不刷审计) */
|
|
13441
|
+
budgetSkips = /* @__PURE__ */ new Set();
|
|
13442
|
+
/** `draft` 档的跳过记录(sanity: PR 创建后回填载体编号) */
|
|
13443
|
+
baselineRedDraft;
|
|
13444
|
+
alertOutcomes = /* @__PURE__ */ new Map();
|
|
12931
13445
|
constructor(options) {
|
|
12932
13446
|
this.workDir = options.workDir ?? process.cwd();
|
|
12933
13447
|
this.reportOutputDir = options.reportOutputDir ?? "./dependfix-reports";
|
|
@@ -12986,7 +13500,11 @@ var DependfixApp = class {
|
|
|
12986
13500
|
attemptTracking: this.attemptTracking,
|
|
12987
13501
|
attemptIndex: this.attemptIndex,
|
|
12988
13502
|
attemptRepo: this.attemptRepo,
|
|
12989
|
-
prCheckWatch: this.prCheckWatchReport
|
|
13503
|
+
prCheckWatch: this.prCheckWatchReport,
|
|
13504
|
+
alertOutcomes: this.alertOutcomes,
|
|
13505
|
+
deliverySkips: this.deliverySkips,
|
|
13506
|
+
deadlineAt: this.config.deadlineAt,
|
|
13507
|
+
budgetSkips: this.budgetSkips
|
|
12990
13508
|
};
|
|
12991
13509
|
}
|
|
12992
13510
|
/** 执行完整的编排流程,返回结构化结果和退出码。 */
|
|
@@ -13081,12 +13599,12 @@ var DependfixApp = class {
|
|
|
13081
13599
|
await this.runRepoPipeline(repositories, (repo) => this.processRepoForFix(client, repo));
|
|
13082
13600
|
if (this.config.commit) {
|
|
13083
13601
|
if (abortBlocksDelivery(this.ctx, "local commit")) return;
|
|
13084
|
-
if (enforceVerificationGate(this.ctx, {
|
|
13602
|
+
if (await enforceVerificationGate(this.ctx, {
|
|
13085
13603
|
preExistingDirty: this.preExistingDirty,
|
|
13086
13604
|
action: "commit"
|
|
13087
13605
|
})) return;
|
|
13088
13606
|
try {
|
|
13089
|
-
commitLocalChanges(this.ctx);
|
|
13607
|
+
await commitLocalChanges(this.ctx);
|
|
13090
13608
|
} catch (error) {
|
|
13091
13609
|
const message = toErrorMessage(error);
|
|
13092
13610
|
this.logger.error(`Local commit failed: ${message}`);
|
|
@@ -13125,24 +13643,27 @@ var DependfixApp = class {
|
|
|
13125
13643
|
if (this.config.cleanupBranches && !this.config.cleanupBranchesAuto) await reportCleanupCandidates(this.ctx, client);
|
|
13126
13644
|
if (this.config.cleanupBranchesAuto) for (const repo of repositories) await autoCleanupMergedBranches(this.ctx, client, repo);
|
|
13127
13645
|
if (abortBlocksDelivery(this.ctx, "PR creation")) return;
|
|
13128
|
-
if (enforceVerificationGate(this.ctx, {
|
|
13646
|
+
if (await enforceVerificationGate(this.ctx, {
|
|
13129
13647
|
preExistingDirty: this.preExistingDirty,
|
|
13130
13648
|
action: "pr"
|
|
13131
13649
|
})) return;
|
|
13132
13650
|
const installBlockedRepos = findInstallBlockedReposWithFixes(this.allActions);
|
|
13133
13651
|
if (installBlockedRepos.length > 0) {
|
|
13134
|
-
|
|
13135
|
-
|
|
13652
|
+
const outcome = await handleBaselineRedDelivery({
|
|
13653
|
+
ctx: this.ctx,
|
|
13654
|
+
config: this.config,
|
|
13655
|
+
client,
|
|
13656
|
+
repositories,
|
|
13657
|
+
installBlockedRepos,
|
|
13136
13658
|
workDir: this.workDir,
|
|
13137
|
-
preExistingDirty: this.preExistingDirty
|
|
13138
|
-
allErrors: this.allErrors,
|
|
13139
|
-
repos: installBlockedRepos
|
|
13659
|
+
preExistingDirty: this.preExistingDirty
|
|
13140
13660
|
});
|
|
13141
|
-
return;
|
|
13661
|
+
if (!outcome.continueAsDraft) return;
|
|
13662
|
+
this.baselineRedDraft = outcome.record;
|
|
13142
13663
|
}
|
|
13143
13664
|
if (!hasDeliverableFixes(this.allActions)) {
|
|
13144
13665
|
this.logger.info("No deliverable fixes produced — skipping PR creation");
|
|
13145
|
-
rollbackForSkippedDelivery({
|
|
13666
|
+
await rollbackForSkippedDelivery({
|
|
13146
13667
|
logger: this.logger,
|
|
13147
13668
|
workDir: this.workDir,
|
|
13148
13669
|
preExistingDirty: this.preExistingDirty,
|
|
@@ -13184,12 +13705,12 @@ var DependfixApp = class {
|
|
|
13184
13705
|
}
|
|
13185
13706
|
if (plan.supersedePRs.length > 0) this.logger.info(`Existing PR(s) with different content will be superseded: ${plan.supersedePRs.map((pr) => `#${pr.number}`).join(", ")}`);
|
|
13186
13707
|
const branchName = `dependfix/auto-fix-${fingerprint}`;
|
|
13187
|
-
createFixBranch(branchName, this.workDir);
|
|
13708
|
+
await createFixBranch(branchName, this.workDir);
|
|
13188
13709
|
this.logger.info(`Creating fix branch: ${branchName}`);
|
|
13189
13710
|
this.logger.info("Staging and committing changes");
|
|
13190
13711
|
const author = this.githubAppAuth?.getCommitAuthor();
|
|
13191
13712
|
try {
|
|
13192
|
-
const scope = stageAndCommit(buildCommitMessage(this.allActions), this.workDir, author, { declaredPaths: collectDeclaredPaths(this.allActions) });
|
|
13713
|
+
const scope = await stageAndCommit(buildCommitMessage(this.allActions), this.workDir, author, { declaredPaths: collectDeclaredPaths(this.allActions) });
|
|
13193
13714
|
if (scope.ignored.length > 0) this.logger.info(`Not committing ${scope.ignored.length} generated / undeclared path(s): ${scope.ignored.join(", ")}`);
|
|
13194
13715
|
if (scope.missing.length > 0) this.logger.warn(`Declared path(s) had no changes in the worktree: ${scope.missing.join(", ")}`);
|
|
13195
13716
|
} catch (error) {
|
|
@@ -13206,7 +13727,7 @@ var DependfixApp = class {
|
|
|
13206
13727
|
throw error;
|
|
13207
13728
|
}
|
|
13208
13729
|
this.logger.info(`Pushing branch: ${branchName}`);
|
|
13209
|
-
pushBranch(branchName, this.workDir);
|
|
13730
|
+
await pushBranch(branchName, this.workDir);
|
|
13210
13731
|
const defaultBranch = await fetchDefaultBranch(client, owner, repo);
|
|
13211
13732
|
computeSummary(this.ctx);
|
|
13212
13733
|
const supplyChainWarnings = collectSupplyChainWarnings(this.workDir, this.allActions);
|
|
@@ -13220,10 +13741,18 @@ var DependfixApp = class {
|
|
|
13220
13741
|
headBranch: branchName,
|
|
13221
13742
|
baseBranch: defaultBranch,
|
|
13222
13743
|
title: prTitle,
|
|
13223
|
-
body: prBody
|
|
13744
|
+
body: prBody,
|
|
13745
|
+
draft: this.baselineRedDraft !== void 0
|
|
13224
13746
|
});
|
|
13225
13747
|
this.logger.info(`PR created: ${pr.htmlUrl}`);
|
|
13226
13748
|
this.summary.prsOpened += 1;
|
|
13749
|
+
if (this.baselineRedDraft) {
|
|
13750
|
+
this.baselineRedDraft.carrier.draftPr = {
|
|
13751
|
+
number: pr.number,
|
|
13752
|
+
htmlUrl: pr.htmlUrl
|
|
13753
|
+
};
|
|
13754
|
+
this.logger.warn(`Draft PR created for pre-existing install failure: ${pr.htmlUrl} — fix the repository install chain, then mark it ready for review`);
|
|
13755
|
+
}
|
|
13227
13756
|
this.allActions.push({
|
|
13228
13757
|
type: "dependency-upgrade",
|
|
13229
13758
|
repository: firstRepo,
|
|
@@ -13243,7 +13772,8 @@ var DependfixApp = class {
|
|
|
13243
13772
|
logger: this.logger
|
|
13244
13773
|
});
|
|
13245
13774
|
await closeSupersededPRs(this.ctx, client, owner, repo, plan.supersedePRs);
|
|
13246
|
-
if (this.config.waitPrChecks)
|
|
13775
|
+
if (this.config.waitPrChecks && this.baselineRedDraft) this.logger.info("Skipping PR check watch: draft delivery is expected to fail on the pre-existing install failure");
|
|
13776
|
+
else if (this.config.waitPrChecks) await runPrCheckWatch({
|
|
13247
13777
|
ctx: this.ctx,
|
|
13248
13778
|
octokit: client,
|
|
13249
13779
|
owner,
|
|
@@ -13360,6 +13890,5 @@ var DependfixApp = class {
|
|
|
13360
13890
|
}
|
|
13361
13891
|
};
|
|
13362
13892
|
|
|
13363
|
-
|
|
13364
|
-
export { ALERT_SOURCES, ATTEMPT_KEY_LENGTH, AUTO_FIXABLE_RULES, AiUsageTracker, CI_HEALTH_MAX_WORKFLOWS, CI_HEALTH_TIMEOUT_MS, CLOSED_ATTEMPT_QUERY_LIMIT, ClosedAttemptIndex, CommitScopeError, DEFAULT_COMMAND_TIMEOUT_MS, DEFAULT_CONCURRENCY, DEFAULT_JOB_BUDGET_MS, DEFAULT_PR_CHECK_RUN_BUDGET_MS, DEFAULT_PR_CHECK_TIMEOUT_MS, DEFAULT_RULES_CONFIG, DEFAULT_RUNTIME_CONFIG, DEFAULT_VERIFY_COMMANDS, DEPENDFIX_BRANCH_PREFIX, DependfixApp, ENV_PREFIX, FAILED_CHECK_SUMMARY_LIMIT, FAILURE_SIGNATURE_INPUT_LIMIT, GIT_COMMIT_HOOKS_ISOLATION_ARGS, GIT_COMMIT_SIGNING_ISOLATION_ARGS, GIT_PUSH_HOOKS_ISOLATION_ARGS, GIT_PUSH_SIGNING_ISOLATION_ARGS, JOB_BUDGET_MAX_SHARE, MAX_COMMAND_TIMEOUT_MS, MAX_CONCURRENCY_LIMIT, MAX_GLOB_PATTERN_LENGTH, MAX_MIRROR_COMMANDS, MAX_RUN_FRAGMENTS, MAX_WORKFLOW_BYTES, MAX_WORKFLOW_FILES, MIN_COMMAND_TIMEOUT_MS, PR_CHECK_GRACE_MS, PR_CHECK_MAX_POLLS, PR_CHECK_POLL_INITIAL_MS, PR_CHECK_POLL_MAX_MS, RULE_CONFIG_ENV, RUNTIME_MODES, SEVERITY_THRESHOLDS, SUGGESTED_RULES, VERIFY_TIMEOUT_ENV, addLabelToPullRequest, analyzeTokenScope, applyCodeScanningFix, applyVersionedOverrides, applyVersionedOverridesBatch, assessBreakingChange, buildCheckFailureComment, buildCiAlignmentRecord, buildPrTitle, buildUpgradeGroups, checkTokenPermissions, chunkPaths, classifyCommandFragment, classifyLockfileFailure, classifyRule, closePullRequest, closeSupersededPRs, collectAttemptMarkers, collectDeclaredPaths, commentOnPullRequest, commitStaged, compareSemver, compareVerificationChains, computeAttemptKey, computeCommitScope, computeFailureSignature, computeFixAndPrPlan, computeFixFingerprint, computeLockfileDiff, computeRetryDelayMs, createFixBranch, createGitHubClient, createPullRequest, dedupeFixableAlerts, deleteRemoteBranch, detectPnpmMajor, discoverRepositories, ensurePnpmOverrides, estimateCostUsd, evaluateCheckTerminal, extractFingerprintFromBranch, extractLockfileVersion, extractMainPackage, extractPrefix, fetchCodeQualityFindings, fetchCodeScanningAlerts, fetchDefaultBranch, fetchDefaultBranchWorkflowHealth, fetchDependabotAlerts, fetchPnpmAuditAlerts, filterExplicitRepositories, findDependencyVersion, findDependfixOpenPR, formatAttemptMarker, formatHistory, formatVerificationError, generatePRBody, getActiveRulesConfig, getBranchPrStatus, getCodeScanningFixTemplate, hashAdvisoryId, inferRepoFromGitRemote, isCommitWhitelisted, isConfirmAnswer, isCrossMajorFixRequired, isGeneratedPath, isMainPackagePresent, isNonSemverDeclaration, isRootDirectDependency, isSafePrototypeKey, isValidConcurrency, isValidPnpmVersion, listChangedPaths, listDependfixBranches, loadRulesConfigFromEnv, loadRulesConfigFromFile, logPartialSourceFailureSummary, mapCodeQualitySeverity, mapGitHubError, maskSecrets, matchesOverrideProtect, matchesPattern, matchesRepoExclude, matchesRepoGlob, matchesRepoInclude, matchesTopicsExclude, mergeRepositories, normalizeCheckRunConclusion, normalizeCommitPath, normalizeCommitStatusConclusion, normalizeOverrideKey, normalizeOverrideSelector, overrideTransitiveDependency, parseAttemptMarkers, parseAuditReport, parseDependabotGroups, parseMajorVersion, parseOverrideProtectEntries, parseRulesConfig, parseWorkflowCommands, partitionSubmanifestAlerts, planCiMirrorCommands, pushBranch, queryRepoHistory, quickVerifyProject, readArchiveIndex, readEnv, readEnvConfig, readExistingOverrides, readLockfileVersion, readLockfileVersions, readReposFile, readWorkflowSources, repairLockfile, repoGlobToRegExp, resetActiveRulesConfig, resolveCommandTimeoutMs, resolveEffectiveTimeout, resolvePnpmVersion, resolveRepoList, resolveRuntimeConfig, resolveWithinWorkDir, restoreSourceFile, restoreTrackedFiles, runVerification, runWithConcurrency, sanitizeCheckName, sanitizeCheckUrl, sanitizeOutput, setActiveRulesConfig, snapshotSourceFile, snapshotTrackedFiles, stageAndCommit, stageScopedChanges, suggestionFor, summarizeVerificationOutput, upgradeDependency, upgradeFamilyDependencies, upsertOverride, watchPullRequestChecks, writeArchive };
|
|
13893
|
+
export { ALERT_SOURCES, ATTEMPT_KEY_LENGTH, AUTO_FIXABLE_RULES, AiUsageTracker, CI_HEALTH_MAX_WORKFLOWS, CI_HEALTH_TIMEOUT_MS, CLOSED_ATTEMPT_QUERY_LIMIT, ClosedAttemptIndex, CommitScopeError, DEFAULT_COMMAND_TIMEOUT_MS, DEFAULT_CONCURRENCY, DEFAULT_JOB_BUDGET_MS, DEFAULT_PR_CHECK_RUN_BUDGET_MS, DEFAULT_PR_CHECK_TIMEOUT_MS, DEFAULT_RULES_CONFIG, DEFAULT_RUNTIME_CONFIG, DEFAULT_VERIFY_COMMANDS, DEPENDFIX_BRANCH_PREFIX, DependfixApp, ENV_PREFIX, FAILED_CHECK_SUMMARY_LIMIT, FAILURE_SIGNATURE_INPUT_LIMIT, GIT_COMMIT_HOOKS_ISOLATION_ARGS, GIT_COMMIT_SIGNING_ISOLATION_ARGS, GIT_PUSH_HOOKS_ISOLATION_ARGS, GIT_PUSH_SIGNING_ISOLATION_ARGS, JOB_BUDGET_MAX_SHARE, MAX_COMMAND_TIMEOUT_MS, MAX_CONCURRENCY_LIMIT, MAX_GLOB_PATTERN_LENGTH, MAX_MIRROR_COMMANDS, MAX_RUN_FRAGMENTS, MAX_WORKFLOW_BYTES, MAX_WORKFLOW_FILES, MIN_COMMAND_TIMEOUT_MS, PR_CHECK_GRACE_MS, PR_CHECK_MAX_POLLS, PR_CHECK_POLL_INITIAL_MS, PR_CHECK_POLL_MAX_MS, RULE_CONFIG_ENV, RUNTIME_MODES, SEVERITY_THRESHOLDS, SUGGESTED_RULES, VERIFY_TIMEOUT_ENV, addLabelToPullRequest, analyzeTokenScope, applyCodeScanningFix, applyVersionedOverrides, applyVersionedOverridesBatch, assessBreakingChange, buildCheckFailureComment, buildCiAlignmentRecord, buildPrTitle, buildUpgradeGroups, checkTokenPermissions, chunkPaths, classifyCommandFragment, classifyLockfileFailure, classifyRule, closePullRequest, closeSupersededPRs, collectAttemptMarkers, collectDeclaredPaths, commentOnPullRequest, commitStaged, compareSemver, compareVerificationChains, computeAttemptKey, computeCommitScope, computeFailureSignature, computeFixAndPrPlan, computeFixFingerprint, computeLockfileDiff, computeRetryDelayMs, createFixBranch, createGitHubClient, createPullRequest, dedupeFixableAlerts, dedupeObservedChecksByLatestRound, deleteRemoteBranch, detectPnpmMajor, discoverRepositories, ensurePnpmOverrides, estimateCostUsd, evaluateCheckTerminal, extractFingerprintFromBranch, extractLockfileVersion, extractMainPackage, extractPrefix, fetchCodeQualityFindings, fetchCodeScanningAlerts, fetchDefaultBranch, fetchDefaultBranchWorkflowHealth, fetchDependabotAlerts, fetchPnpmAuditAlerts, filterExplicitRepositories, findDependencyVersion, findDependfixOpenPR, formatAttemptMarker, formatHistory, formatVerificationError, generatePRBody, getActiveRulesConfig, getBranchPrStatus, getCodeScanningFixTemplate, hashAdvisoryId, inferRepoFromGitRemote, isCommitWhitelisted, isConfirmAnswer, isCrossMajorFixRequired, isGeneratedPath, isMainPackagePresent, isNonSemverDeclaration, isRootDirectDependency, isSafePrototypeKey, isValidConcurrency, isValidPnpmVersion, listChangedPaths, listDependfixBranches, loadRulesConfigFromEnv, loadRulesConfigFromFile, logPartialSourceFailureSummary, mapCodeQualitySeverity, mapGitHubError, maskSecrets, matchesOverrideProtect, matchesPattern, matchesRepoExclude, matchesRepoGlob, matchesRepoInclude, matchesTopicsExclude, mergeRepositories, normalizeCheckRunConclusion, normalizeCommitPath, normalizeCommitStatusConclusion, normalizeOverrideKey, normalizeOverrideSelector, overrideTransitiveDependency, parseAttemptMarkers, parseAuditReport, parseDependabotGroups, parseMajorVersion, parseOverrideProtectEntries, parseRulesConfig, parseWorkflowCommands, partitionSubmanifestAlerts, planCiMirrorCommands, pushBranch, queryRepoHistory, quickVerifyProject, readArchiveIndex, readEnv, readEnvConfig, readExistingOverrides, readLockfileVersion, readLockfileVersions, readReposFile, readWorkflowSources, repairLockfile, repoGlobToRegExp, resetActiveRulesConfig, resolveCommandTimeoutMs, resolveEffectiveTimeout, resolvePnpmVersion, resolveRepoList, resolveRuntimeConfig, resolveWithinWorkDir, restoreSourceFile, restoreTrackedFiles, runVerification, runWithConcurrency, sanitizeCheckName, sanitizeCheckUrl, sanitizeOutput, setActiveRulesConfig, snapshotSourceFile, snapshotTrackedFiles, stageAndCommit, stageScopedChanges, suggestionFor, summarizeVerificationOutput, upgradeDependency, upgradeFamilyDependencies, upsertOverride, watchPullRequestChecks, writeArchive };
|
|
13365
13894
|
//# sourceMappingURL=index.mjs.map
|