sela-core 0.1.0-alpha.31 → 0.1.0-alpha.33
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/cli/commands/pr.d.ts +16 -0
- package/dist/cli/commands/pr.d.ts.map +1 -0
- package/dist/cli/commands/pr.js +81 -0
- package/dist/cli/index.js +3 -0
- package/dist/engine/SelaEngine.d.ts.map +1 -1
- package/dist/engine/SelaEngine.js +10 -59
- package/dist/reporter/SelaReporter.d.ts +17 -1
- package/dist/reporter/SelaReporter.d.ts.map +1 -1
- package/dist/reporter/SelaReporter.js +69 -5
- package/dist/services/PRAutomationService.d.ts +16 -0
- package/dist/services/PRAutomationService.d.ts.map +1 -1
- package/dist/services/PRAutomationService.js +99 -0
- package/dist/services/PendingPromptLedger.d.ts +32 -1
- package/dist/services/PendingPromptLedger.d.ts.map +1 -1
- package/dist/services/PendingPromptLedger.js +67 -10
- package/package.json +1 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* `sela pr aggregate --ledger-dir <path>`
|
|
4
|
+
*
|
|
5
|
+
* CI fan-in: after all sharded Playwright runners complete and upload their
|
|
6
|
+
* `.sela/pending-prompts` artifacts, the downstream merge job downloads
|
|
7
|
+
* everything into one directory and calls this command to open a single
|
|
8
|
+
* unified PR for the entire test suite.
|
|
9
|
+
*
|
|
10
|
+
* Typical GHA usage:
|
|
11
|
+
* - Upload: actions/upload-artifact@v4 path: .sela/pending-prompts/
|
|
12
|
+
* - Download: actions/download-artifact@v4 merge-multiple: true path: sela-ledgers/
|
|
13
|
+
* - Run: npx sela pr aggregate --ledger-dir ./sela-ledgers
|
|
14
|
+
*/
|
|
15
|
+
export declare function registerPr(parent: Command): void;
|
|
16
|
+
//# sourceMappingURL=pr.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pr.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/pr.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAazC;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAgDhD"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.registerPr = registerPr;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const PendingPromptLedger_js_1 = require("../../services/PendingPromptLedger.js");
|
|
39
|
+
const SelaReporter_js_1 = require("../../reporter/SelaReporter.js");
|
|
40
|
+
const PRAutomationService_js_1 = require("../../services/PRAutomationService.js");
|
|
41
|
+
/**
|
|
42
|
+
* `sela pr aggregate --ledger-dir <path>`
|
|
43
|
+
*
|
|
44
|
+
* CI fan-in: after all sharded Playwright runners complete and upload their
|
|
45
|
+
* `.sela/pending-prompts` artifacts, the downstream merge job downloads
|
|
46
|
+
* everything into one directory and calls this command to open a single
|
|
47
|
+
* unified PR for the entire test suite.
|
|
48
|
+
*
|
|
49
|
+
* Typical GHA usage:
|
|
50
|
+
* - Upload: actions/upload-artifact@v4 path: .sela/pending-prompts/
|
|
51
|
+
* - Download: actions/download-artifact@v4 merge-multiple: true path: sela-ledgers/
|
|
52
|
+
* - Run: npx sela pr aggregate --ledger-dir ./sela-ledgers
|
|
53
|
+
*/
|
|
54
|
+
function registerPr(parent) {
|
|
55
|
+
const pr = parent
|
|
56
|
+
.command("pr")
|
|
57
|
+
.description("Pull-request automation commands");
|
|
58
|
+
pr.command("aggregate")
|
|
59
|
+
.description("Aggregate sharded ledger files and open a single unified PR (CI fan-in)")
|
|
60
|
+
.requiredOption("--ledger-dir <path>", "Directory containing all downloaded shard ledger JSON files")
|
|
61
|
+
.option("--cwd <path>", "Repository root used for git operations (defaults to process.cwd())", process.cwd())
|
|
62
|
+
.action(async (opts) => {
|
|
63
|
+
const ledgerDir = path.resolve(opts.cwd, opts.ledgerDir);
|
|
64
|
+
const payloads = (0, PendingPromptLedger_js_1.readGitPayloadsFromDir)(ledgerDir);
|
|
65
|
+
if (payloads.length === 0) {
|
|
66
|
+
console.error(`[sela pr aggregate] No ledger payloads found in ${ledgerDir} — nothing to do.`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const healedEvents = (0, SelaReporter_js_1.deduplicateByStableId)(payloads.flatMap((p) => p.healedEvents));
|
|
70
|
+
if (healedEvents.length === 0) {
|
|
71
|
+
console.error("[sela pr aggregate] No healed events after deduplication — nothing to do.");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const protectedEvents = payloads.flatMap((p) => p.protectedEvents);
|
|
75
|
+
await (0, PRAutomationService_js_1.runCentralizedGitFlow)({
|
|
76
|
+
cwd: opts.cwd,
|
|
77
|
+
healedEvents,
|
|
78
|
+
protectedEvents,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -54,6 +54,7 @@ const bulk_js_1 = require("./commands/bulk.js");
|
|
|
54
54
|
const sync_js_1 = require("./commands/sync.js");
|
|
55
55
|
const showReport_js_1 = require("./commands/showReport.js");
|
|
56
56
|
const merge_js_1 = require("./commands/merge.js");
|
|
57
|
+
const pr_js_1 = require("./commands/pr.js");
|
|
57
58
|
(0, ErrorHandler_js_1.registerGlobalHandlers)();
|
|
58
59
|
// Safety net: route any stray third-party console.log through the Logger's
|
|
59
60
|
// verbosity-gated debug channel (stderr) so stdout stays pipe-clean and the
|
|
@@ -77,6 +78,8 @@ program
|
|
|
77
78
|
(0, showReport_js_1.registerShowReport)(program);
|
|
78
79
|
// ── sela merge - CI fan-in: aggregate sharded reports into one ────────────────
|
|
79
80
|
(0, merge_js_1.registerMerge)(program);
|
|
81
|
+
// ── sela pr <subcommand> - pull-request automation ───────────────────────────
|
|
82
|
+
(0, pr_js_1.registerPr)(program);
|
|
80
83
|
// ── sela dna <subcommand> ─────────────────────────────────────────────────────
|
|
81
84
|
const dna = new commander_1.Command("dna")
|
|
82
85
|
.description("DNA management commands")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelaEngine.d.ts","sourceRoot":"","sources":["../../src/engine/SelaEngine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"SelaEngine.d.ts","sourceRoot":"","sources":["../../src/engine/SelaEngine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAcxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAG7C,OAAO,EAAe,QAAQ,EAAkB,MAAM,yBAAyB,CAAC;AAoFhF,qBAAa,UAAU;IACrB,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,mBAAmB,CAAmB;IAC9C,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,SAAS,CAA6C;IAC9D,OAAO,CAAC,cAAc,CAAoC;IAC1D,OAAO,CAAC,eAAe,CAAkC;;IAuBnD,YAAY,CAChB,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA0DnB,IAAI,CACR,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,MAAM,EACpB,mBAAmB,EAAE,MAAM,EAC3B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,QAAmB,GAC5B,OAAO,CAAC,MAAM,CAAC;IAirBlB,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,aAAa;IAerB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,oBAAoB;IAoB5B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,2BAA2B;IA4BnC,OAAO,CAAC,yBAAyB;IAgDjC,OAAO,CAAC,kBAAkB;IA8D1B,OAAO,CAAC,oBAAoB;IAyD5B,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAIrD,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;YAoGtB,aAAa;IAsB3B,OAAO,CAAC,0BAA0B;IA+BlC,OAAO,CAAC,WAAW;IA6Bb,wBAAwB,CAC5B,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM;CAqBnB;AAED,OAAO,EAAE,CAAC"}
|
|
@@ -42,7 +42,6 @@ const IsolatedDiff_1 = require("../utils/IsolatedDiff");
|
|
|
42
42
|
const logger_1 = require("../utils/logger");
|
|
43
43
|
const SnapshotService_1 = require("../services/SnapshotService");
|
|
44
44
|
const DOMUtils_1 = require("../utils/DOMUtils");
|
|
45
|
-
const FileLock_1 = require("../utils/FileLock");
|
|
46
45
|
const SourceUpdater_1 = require("../services/SourceUpdater");
|
|
47
46
|
const ASTSourceUpdater_1 = require("../services/ASTSourceUpdater");
|
|
48
47
|
const SafetyGuard_1 = require("../services/SafetyGuard");
|
|
@@ -51,7 +50,6 @@ const ConfigLoader_1 = require("../config/ConfigLoader");
|
|
|
51
50
|
const DryRunGuard_1 = require("../config/DryRunGuard");
|
|
52
51
|
const HealBudget_1 = require("../services/HealBudget");
|
|
53
52
|
const HealReportService_1 = require("../services/HealReportService");
|
|
54
|
-
const PRAutomationService_1 = require("../services/PRAutomationService");
|
|
55
53
|
const PendingPromptLedger_1 = require("../services/PendingPromptLedger");
|
|
56
54
|
const SelaError_1 = require("../errors/SelaError");
|
|
57
55
|
const SelectorSanitizer_1 = require("../services/SelectorSanitizer");
|
|
@@ -979,70 +977,23 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
979
977
|
}
|
|
980
978
|
}
|
|
981
979
|
}
|
|
982
|
-
// ──
|
|
980
|
+
// ── Cross-process worker ledger ──────────────────────────────────────
|
|
983
981
|
//
|
|
984
|
-
//
|
|
985
|
-
//
|
|
986
|
-
//
|
|
987
|
-
//
|
|
988
|
-
//
|
|
982
|
+
// Workers serialise their state to a per-worker JSON file so the master
|
|
983
|
+
// process (SelaReporter.onEnd) can aggregate everything after all workers
|
|
984
|
+
// have exited. The ledger carries:
|
|
985
|
+
// • PendingPromptEntry[] — for local-DX diff display / vscode deeplinks.
|
|
986
|
+
// • WorkerGitPayload — for CI centralized git/PR flow.
|
|
989
987
|
//
|
|
990
|
-
//
|
|
991
|
-
//
|
|
992
|
-
|
|
993
|
-
if (isCiEnvironment()) {
|
|
994
|
-
if (this.config.prAutomation.enabled) {
|
|
995
|
-
// SELA-5: serialise PR automation (git stash / checkout / commit /
|
|
996
|
-
// push) behind a repo-root lock. CI normally runs one worker per
|
|
997
|
-
// sharded machine, so the lock is uncontended; if a machine is
|
|
998
|
-
// misconfigured with workers>1 it prevents concurrent git operations
|
|
999
|
-
// from corrupting the index / worktree. NOTE: this guards against git
|
|
1000
|
-
// corruption only — aggregating heals from multiple workers into a
|
|
1001
|
-
// single PR is intentionally out of scope (run one worker per shard).
|
|
1002
|
-
const prLock = new FileLock_1.FileLock((0, FileLock_1.lockDirFor)(path.join(process.cwd(), ".sela-pr")), { staleMs: 120_000 });
|
|
1003
|
-
try {
|
|
1004
|
-
const ran = await prLock.withLock(async () => {
|
|
1005
|
-
const workerIdx = process.env["TEST_WORKER_INDEX"];
|
|
1006
|
-
if (workerIdx !== undefined && workerIdx !== "0") {
|
|
1007
|
-
logger_1.logger.warn("[PR] PR automation under workers>1 is unsupported — " +
|
|
1008
|
-
"run one worker per CI shard. Serialising to protect the git " +
|
|
1009
|
-
"index; heals from other workers may land in separate commits.");
|
|
1010
|
-
}
|
|
1011
|
-
const branchInfo = (0, PRAutomationService_1.detectBranch)(process.cwd());
|
|
1012
|
-
const decision = (0, PRAutomationService_1.decideEffectiveStrategy)(this.config.prAutomation, branchInfo.branch, healedEvents);
|
|
1013
|
-
if (decision.downgradeReason) {
|
|
1014
|
-
logger_1.logger.warn(`[PR] Strategy downgraded: '${decision.configured}' → '${decision.effective}' ` +
|
|
1015
|
-
`(reason: ${decision.downgradeReason}, ` +
|
|
1016
|
-
`minAI: ${fmtPctEngine(decision.minAiConfidence)}, ` +
|
|
1017
|
-
`minAuditor: ${fmtPctEngine(decision.minAuditorConfidence)})`);
|
|
1018
|
-
}
|
|
1019
|
-
else {
|
|
1020
|
-
logger_1.logger.debug(`[PR] Effective strategy: '${decision.effective}' on branch '${branchInfo.branch ?? "unknown"}'`);
|
|
1021
|
-
}
|
|
1022
|
-
const ctx = { cwd: process.cwd(), reportHtmlPath };
|
|
1023
|
-
await (0, PRAutomationService_1.execute)(this.config.prAutomation, decision, healedEvents, branchInfo, ctx);
|
|
1024
|
-
await (0, PRAutomationService_1.handleBugDetected)(this.config.prAutomation.onBugDetected, protectedEvents, branchInfo, ctx);
|
|
1025
|
-
});
|
|
1026
|
-
if (ran === undefined) {
|
|
1027
|
-
logger_1.logger.warn("[PR] Skipped PR automation — another worker holds the PR lock.");
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
catch (err) {
|
|
1031
|
-
logger_1.logger.warn(`[PR] PR automation step failed: ${err.message}`);
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
else if (healedEvents.length > 0) {
|
|
1036
|
-
// Local DX: serialise the mutated snapshots to a cross-process
|
|
1037
|
-
// ledger. The SelaReporter drains it from `onEnd()` in the main
|
|
1038
|
-
// process and prints a non-blocking summary (diff + vscode://
|
|
1039
|
-
// deeplink). Mutations are already on disk - the ledger only
|
|
1040
|
-
// exists so the reporter knows what to narrate.
|
|
988
|
+
// Both CI and local paths write the ledger. The master process decides
|
|
989
|
+
// which parts to consume based on its own environment check.
|
|
990
|
+
if (healedEvents.length > 0 || protectedEvents.length > 0) {
|
|
1041
991
|
try {
|
|
1042
992
|
(0, PendingPromptLedger_1.writePendingPromptsLedger)({
|
|
1043
993
|
cwd: process.cwd(),
|
|
1044
994
|
workspace: WorkspaceSnapshotService_1.sharedWorkspaceSnapshot,
|
|
1045
995
|
healedEvents,
|
|
996
|
+
protectedEvents,
|
|
1046
997
|
});
|
|
1047
998
|
}
|
|
1048
999
|
catch (err) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Reporter, TestCase, TestResult, FullConfig, Suite, FullResult } from "@playwright/test/reporter";
|
|
2
2
|
import { HealReportService } from "../services/HealReportService.js";
|
|
3
3
|
import type { SelaEngine } from "../engine/SelaEngine.js";
|
|
4
|
-
import { type PendingPromptEntry } from "../services/PendingPromptLedger.js";
|
|
4
|
+
import { type PendingPromptEntry, type WorkerGitPayload } from "../services/PendingPromptLedger.js";
|
|
5
5
|
import { type PrintReviewDeps } from "../services/InteractiveReview.js";
|
|
6
6
|
export interface ReporterEngine {
|
|
7
7
|
commitUpdates(): Promise<void>;
|
|
@@ -61,6 +61,12 @@ export interface SelaReporterOpts {
|
|
|
61
61
|
* polluted by leftover handlers between cases.
|
|
62
62
|
*/
|
|
63
63
|
installSignalHandlers?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Override the centralized git/PR flow executed in CI after all workers
|
|
66
|
+
* finish. Tests stub this to avoid real git ops. Defaults to reading the
|
|
67
|
+
* worker ledger and calling PRAutomationService.runCentralizedGitFlow.
|
|
68
|
+
*/
|
|
69
|
+
runCentralizedGitFlow?: (cwd: string) => Promise<void>;
|
|
64
70
|
}
|
|
65
71
|
export declare class SelaReporter implements Reporter {
|
|
66
72
|
private readonly engine;
|
|
@@ -75,8 +81,10 @@ export declare class SelaReporter implements Reporter {
|
|
|
75
81
|
private readonly isCi;
|
|
76
82
|
private readonly mergeShards;
|
|
77
83
|
private readonly shouldInstallSignalHandlers;
|
|
84
|
+
private readonly runCentralizedGitFlowFn;
|
|
78
85
|
private shutdownHandler;
|
|
79
86
|
private shuttingDown;
|
|
87
|
+
private _isShardedRun;
|
|
80
88
|
/** Total tests in the suite, captured at onBegin for the end-of-run summary. */
|
|
81
89
|
private totalTests;
|
|
82
90
|
/**
|
|
@@ -161,5 +169,13 @@ export declare class SelaReporter implements Reporter {
|
|
|
161
169
|
*/
|
|
162
170
|
private extractScreenshotBase64;
|
|
163
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Deduplicate HealedEvents by stableId. When the same selector was healed
|
|
174
|
+
* across multiple retries or workers, the last entry wins — it represents
|
|
175
|
+
* the canonical final state of that mutation.
|
|
176
|
+
*
|
|
177
|
+
* Exported for use by `sela pr aggregate` (cross-shard fan-in).
|
|
178
|
+
*/
|
|
179
|
+
export declare function deduplicateByStableId(events: WorkerGitPayload["healedEvents"]): WorkerGitPayload["healedEvents"];
|
|
164
180
|
export default SelaReporter;
|
|
165
181
|
//# sourceMappingURL=SelaReporter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelaReporter.d.ts","sourceRoot":"","sources":["../../src/reporter/SelaReporter.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EACV,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,UAAU,EACV,KAAK,EACL,UAAU,EACX,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAEL,iBAAiB,EAIlB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAE1D,OAAO,
|
|
1
|
+
{"version":3,"file":"SelaReporter.d.ts","sourceRoot":"","sources":["../../src/reporter/SelaReporter.ts"],"names":[],"mappings":"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"}
|
|
@@ -62,6 +62,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
62
62
|
})();
|
|
63
63
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
64
64
|
exports.SelaReporter = void 0;
|
|
65
|
+
exports.deduplicateByStableId = deduplicateByStableId;
|
|
65
66
|
const fs = __importStar(require("fs"));
|
|
66
67
|
const path = __importStar(require("path"));
|
|
67
68
|
const HealReportService_js_1 = require("../services/HealReportService.js");
|
|
@@ -69,6 +70,7 @@ const ReportMergeService_js_1 = require("../services/ReportMergeService.js");
|
|
|
69
70
|
const singleton_js_1 = require("../engine/singleton.js");
|
|
70
71
|
const SelaError_js_1 = require("../errors/SelaError.js");
|
|
71
72
|
const PendingPromptLedger_js_1 = require("../services/PendingPromptLedger.js");
|
|
73
|
+
const PRAutomationService_js_1 = require("../services/PRAutomationService.js");
|
|
72
74
|
const InteractiveReview_js_1 = require("../services/InteractiveReview.js");
|
|
73
75
|
const logger_js_1 = require("../utils/logger.js");
|
|
74
76
|
const SummaryTable_js_1 = require("../utils/SummaryTable.js");
|
|
@@ -88,8 +90,10 @@ class SelaReporter {
|
|
|
88
90
|
isCi;
|
|
89
91
|
mergeShards;
|
|
90
92
|
shouldInstallSignalHandlers;
|
|
93
|
+
runCentralizedGitFlowFn;
|
|
91
94
|
shutdownHandler = null;
|
|
92
95
|
shuttingDown = false;
|
|
96
|
+
_isShardedRun = false;
|
|
93
97
|
/** Total tests in the suite, captured at onBegin for the end-of-run summary. */
|
|
94
98
|
totalTests = 0;
|
|
95
99
|
/**
|
|
@@ -120,6 +124,8 @@ class SelaReporter {
|
|
|
120
124
|
this.isCi = opts.isCi ?? defaultIsCi;
|
|
121
125
|
this.mergeShards = opts.mergeShards ?? defaultMergeShards;
|
|
122
126
|
this.shouldInstallSignalHandlers = opts.installSignalHandlers ?? true;
|
|
127
|
+
this.runCentralizedGitFlowFn =
|
|
128
|
+
opts.runCentralizedGitFlow ?? defaultRunCentralizedGitFlow;
|
|
123
129
|
}
|
|
124
130
|
// ─────────────────────────────────────────────────────────────
|
|
125
131
|
// Playwright Reporter API
|
|
@@ -129,6 +135,11 @@ class SelaReporter {
|
|
|
129
135
|
// finalisation. Today the fixture still calls engine.commitUpdates()
|
|
130
136
|
// itself; the env flag is reserved for the planned dedup guard.
|
|
131
137
|
process.env.SELA_REPORTER_ACTIVE = "1";
|
|
138
|
+
// Detect Playwright CI sharding (--shard=N/M). When sharded, each runner
|
|
139
|
+
// must skip the Git/PR flow and let the downstream fan-in job handle it.
|
|
140
|
+
if (_config?.shard != null) {
|
|
141
|
+
this._isShardedRun = true;
|
|
142
|
+
}
|
|
132
143
|
// Snapshot the suite size for the end-of-run summary table. Guarded:
|
|
133
144
|
// allTests() exists on real Playwright suites but not on test stubs.
|
|
134
145
|
try {
|
|
@@ -218,6 +229,25 @@ class SelaReporter {
|
|
|
218
229
|
this.logger.error(`[Sela] shard merge failed: ${msg}`);
|
|
219
230
|
}
|
|
220
231
|
}
|
|
232
|
+
// ── CI: centralized git/PR flow ──────────────────────────────
|
|
233
|
+
//
|
|
234
|
+
// All workers have exited by the time onEnd() fires. Their git-flow
|
|
235
|
+
// payloads are aggregated from the per-worker ledger files and fed
|
|
236
|
+
// into a single runCentralizedGitFlow call — no lock needed, no race
|
|
237
|
+
// on the git index, no "another worker holds the PR lock" warning.
|
|
238
|
+
//
|
|
239
|
+
// Sharded CI runs (--shard=N/M) skip this entirely: each shard runner
|
|
240
|
+
// only flushes its ledger; the downstream `sela pr aggregate` fan-in
|
|
241
|
+
// job opens the single unified PR after all shards complete.
|
|
242
|
+
if (this.isCi() && !this._isShardedRun) {
|
|
243
|
+
try {
|
|
244
|
+
await this.runCentralizedGitFlowFn(this.cwd);
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
248
|
+
this.logger.error(`[Sela] centralized git flow failed: ${msg}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
221
251
|
// ── Local DX - Write-and-notify summary ─────────────────────
|
|
222
252
|
//
|
|
223
253
|
// Workers serialise their mutated FileSnapshots to
|
|
@@ -226,8 +256,7 @@ class SelaReporter {
|
|
|
226
256
|
// deeplink + HTML report deeplink). No Y/N prompt - the files are
|
|
227
257
|
// already mutated on disk by the time we get here.
|
|
228
258
|
//
|
|
229
|
-
// CI runs skip the notify entirely - PR automation
|
|
230
|
-
// the mutations on its isolated branch.
|
|
259
|
+
// CI runs skip the notify entirely - PR automation is handled above.
|
|
231
260
|
if (!this.isCi()) {
|
|
232
261
|
try {
|
|
233
262
|
const entries = this.readLedger(this.cwd);
|
|
@@ -249,9 +278,11 @@ class SelaReporter {
|
|
|
249
278
|
}
|
|
250
279
|
}
|
|
251
280
|
}
|
|
252
|
-
else {
|
|
253
|
-
// CI
|
|
254
|
-
// mutations from a previous CI session.
|
|
281
|
+
else if (!this._isShardedRun) {
|
|
282
|
+
// Non-sharded CI: drop the ledger so a follow-up local run doesn't
|
|
283
|
+
// replay mutations from a previous CI session.
|
|
284
|
+
// Sharded runs must NOT clear here — the ledger files must survive
|
|
285
|
+
// until actions/upload-artifact captures them for the fan-in job.
|
|
255
286
|
try {
|
|
256
287
|
this.clearLedger(this.cwd);
|
|
257
288
|
}
|
|
@@ -512,6 +543,39 @@ function defaultMergeShards(cwd) {
|
|
|
512
543
|
(0, HealReportService_js_1.clearReportShards)(cwd);
|
|
513
544
|
}
|
|
514
545
|
}
|
|
546
|
+
/**
|
|
547
|
+
* Default centralized git flow: aggregate all workers' git payloads from the
|
|
548
|
+
* pending-prompts ledger, deduplicate healed events by stableId (last retry
|
|
549
|
+
* wins), then delegate to PRAutomationService.runCentralizedGitFlow.
|
|
550
|
+
*/
|
|
551
|
+
async function defaultRunCentralizedGitFlow(cwd) {
|
|
552
|
+
const payloads = (0, PendingPromptLedger_js_1.readGitPayloads)(cwd);
|
|
553
|
+
if (payloads.length === 0)
|
|
554
|
+
return;
|
|
555
|
+
const allHealedEvents = deduplicateByStableId(payloads.flatMap((p) => p.healedEvents));
|
|
556
|
+
const allProtectedEvents = payloads.flatMap((p) => p.protectedEvents);
|
|
557
|
+
if (allHealedEvents.length === 0)
|
|
558
|
+
return;
|
|
559
|
+
await (0, PRAutomationService_js_1.runCentralizedGitFlow)({
|
|
560
|
+
cwd,
|
|
561
|
+
healedEvents: allHealedEvents,
|
|
562
|
+
protectedEvents: allProtectedEvents,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Deduplicate HealedEvents by stableId. When the same selector was healed
|
|
567
|
+
* across multiple retries or workers, the last entry wins — it represents
|
|
568
|
+
* the canonical final state of that mutation.
|
|
569
|
+
*
|
|
570
|
+
* Exported for use by `sela pr aggregate` (cross-shard fan-in).
|
|
571
|
+
*/
|
|
572
|
+
function deduplicateByStableId(events) {
|
|
573
|
+
const seen = new Map();
|
|
574
|
+
for (const ev of events) {
|
|
575
|
+
seen.set(ev.stableId, ev);
|
|
576
|
+
}
|
|
577
|
+
return [...seen.values()];
|
|
578
|
+
}
|
|
515
579
|
// Default export so Playwright can resolve `reporter: [['sela-core/reporter']]`
|
|
516
580
|
// without needing the consumer to spell out the named import.
|
|
517
581
|
exports.default = SelaReporter;
|
|
@@ -47,4 +47,20 @@ export declare function execute(cfg: ResolvedPRAutomation, decision: StrategyDec
|
|
|
47
47
|
/** Injectable for tests - defaults to the module singleton. */
|
|
48
48
|
workspace?: WorkspaceSnapshotService): Promise<ExecutionResult>;
|
|
49
49
|
export declare function handleBugDetected(action: BugAction, events: ProtectedEvent[], branchInfo: BranchInfo, ctx: ExecuteContext): Promise<void>;
|
|
50
|
+
export interface CentralizedGitFlowOpts {
|
|
51
|
+
cwd: string;
|
|
52
|
+
healedEvents: HealedEvent[];
|
|
53
|
+
protectedEvents: ProtectedEvent[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Run the full git/PR automation ONCE in the master process after all
|
|
57
|
+
* Playwright workers have exited. Called from SelaReporter.onEnd().
|
|
58
|
+
*
|
|
59
|
+
* Unlike the old per-worker path this function:
|
|
60
|
+
* - Needs no FileLock (single caller, single process).
|
|
61
|
+
* - Resolves `contentBefore` via `git show HEAD` — retry-proof, never
|
|
62
|
+
* polluted by a prior MUTATE_IN_PLACE run on the same branch.
|
|
63
|
+
* - Accepts pre-aggregated + de-duplicated events from all workers.
|
|
64
|
+
*/
|
|
65
|
+
export declare function runCentralizedGitFlow(opts: CentralizedGitFlowOpts): Promise<void>;
|
|
50
66
|
//# sourceMappingURL=PRAutomationService.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PRAutomationService.d.ts","sourceRoot":"","sources":["../../src/services/PRAutomationService.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"PRAutomationService.d.ts","sourceRoot":"","sources":["../../src/services/PRAutomationService.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EACV,oBAAoB,EACpB,UAAU,EACV,SAAS,EACV,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,4BAA4B,CAAC;AAQpC,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,UAAU,CAAC;IACtB,UAAU,EAAE,UAAU,CAAC;IACvB,eAAe,EAAE,kBAAkB,GAAG,gBAAgB,GAAG,aAAa,GAAG,IAAI,CAAC;IAC9E;;;;OAIG;IACH,eAAe,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,6EAA6E;IAC7E,oBAAoB,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1C;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAiFD,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAsB,GAAG,UAAU,CA+EpE;AAMD,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,oBAAoB,EACzB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,WAAW,EAAE,GACnB,gBAAgB,CAgElB;AAMD,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAEzC;AA6FD,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,UAAU,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;GASG;AACH,wBAAsB,OAAO,CAC3B,GAAG,EAAE,oBAAoB,EACzB,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,WAAW,EAAE,EACpB,UAAU,EAAE,UAAU,EACtB,GAAG,EAAE,cAAc;AACnB,+DAA+D;AAC/D,SAAS,GAAE,wBAAkD,GAC5D,OAAO,CAAC,eAAe,CAAC,CAkG1B;AAsTD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,cAAc,EAAE,EACxB,UAAU,EAAE,UAAU,EACtB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,IAAI,CAAC,CAgGf;AAMD,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,eAAe,EAAE,cAAc,EAAE,CAAC;CACnC;AAED;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAgDf"}
|
|
@@ -51,9 +51,11 @@ exports.decideEffectiveStrategy = decideEffectiveStrategy;
|
|
|
51
51
|
exports.fmtPct = fmtPct;
|
|
52
52
|
exports.execute = execute;
|
|
53
53
|
exports.handleBugDetected = handleBugDetected;
|
|
54
|
+
exports.runCentralizedGitFlow = runCentralizedGitFlow;
|
|
54
55
|
const fs = __importStar(require("fs"));
|
|
55
56
|
const path = __importStar(require("path"));
|
|
56
57
|
const child_process_1 = require("child_process");
|
|
58
|
+
const ConfigLoader_1 = require("../config/ConfigLoader");
|
|
57
59
|
const WorkspaceSnapshotService_1 = require("./WorkspaceSnapshotService");
|
|
58
60
|
const IsolatedDiff_1 = require("../utils/IsolatedDiff");
|
|
59
61
|
const logger_1 = require("../utils/logger");
|
|
@@ -746,6 +748,103 @@ async function handleBugDetected(action, events, branchInfo, ctx) {
|
|
|
746
748
|
}
|
|
747
749
|
}
|
|
748
750
|
}
|
|
751
|
+
/**
|
|
752
|
+
* Run the full git/PR automation ONCE in the master process after all
|
|
753
|
+
* Playwright workers have exited. Called from SelaReporter.onEnd().
|
|
754
|
+
*
|
|
755
|
+
* Unlike the old per-worker path this function:
|
|
756
|
+
* - Needs no FileLock (single caller, single process).
|
|
757
|
+
* - Resolves `contentBefore` via `git show HEAD` — retry-proof, never
|
|
758
|
+
* polluted by a prior MUTATE_IN_PLACE run on the same branch.
|
|
759
|
+
* - Accepts pre-aggregated + de-duplicated events from all workers.
|
|
760
|
+
*/
|
|
761
|
+
async function runCentralizedGitFlow(opts) {
|
|
762
|
+
const cfg = ConfigLoader_1.ConfigLoader.getInstance();
|
|
763
|
+
const prCfg = cfg.prAutomation;
|
|
764
|
+
if (!prCfg.enabled)
|
|
765
|
+
return;
|
|
766
|
+
if (opts.healedEvents.length === 0)
|
|
767
|
+
return;
|
|
768
|
+
const fileSnapshots = resolveGitBaselines(opts.healedEvents, opts.cwd);
|
|
769
|
+
if (fileSnapshots.length === 0) {
|
|
770
|
+
logger_1.logger.warn("[PR] Centralized flow: no mutated files resolved from git baseline — skipping.");
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const branchInfo = detectBranch(opts.cwd);
|
|
774
|
+
const decision = decideEffectiveStrategy(prCfg, branchInfo.branch, opts.healedEvents);
|
|
775
|
+
if (decision.downgradeReason) {
|
|
776
|
+
logger_1.logger.warn(`[PR] Strategy downgraded: '${decision.configured}' → '${decision.effective}' ` +
|
|
777
|
+
`(reason: ${decision.downgradeReason}, ` +
|
|
778
|
+
`minAI: ${fmtPct(decision.minAiConfidence)}, ` +
|
|
779
|
+
`minAuditor: ${fmtPct(decision.minAuditorConfidence)})`);
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
logger_1.logger.debug(`[PR] Effective strategy: '${decision.effective}' on branch '${branchInfo.branch ?? "unknown"}'`);
|
|
783
|
+
}
|
|
784
|
+
// Minimal workspace shim — execute() only calls getMutatedSnapshots().
|
|
785
|
+
const fakeWorkspace = {
|
|
786
|
+
getMutatedSnapshots: () => fileSnapshots,
|
|
787
|
+
};
|
|
788
|
+
const ctx = { cwd: opts.cwd, reportHtmlPath: null };
|
|
789
|
+
try {
|
|
790
|
+
await execute(prCfg, decision, opts.healedEvents, branchInfo, ctx, fakeWorkspace);
|
|
791
|
+
await handleBugDetected(prCfg.onBugDetected, opts.protectedEvents, branchInfo, ctx);
|
|
792
|
+
}
|
|
793
|
+
catch (err) {
|
|
794
|
+
logger_1.logger.warn(`[PR] Centralized git flow failed: ${err?.message ?? err}`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Build FileSnapshot[] for the centralized git flow by resolving each
|
|
799
|
+
* mutated file's baseline from `git show HEAD`. This is retry-proof:
|
|
800
|
+
* HEAD always reflects the committed state before any worker's
|
|
801
|
+
* MUTATE_IN_PLACE touched the working tree.
|
|
802
|
+
*
|
|
803
|
+
* contentBefore = headContent = git HEAD content (CI worktree is clean).
|
|
804
|
+
* contentAfter = current file on disk (workers wrote mutations in-place).
|
|
805
|
+
*/
|
|
806
|
+
function resolveGitBaselines(heals, cwd) {
|
|
807
|
+
const filePaths = new Set();
|
|
808
|
+
for (const h of heals) {
|
|
809
|
+
const target = h.definitionSite?.file ?? h.sourceFile;
|
|
810
|
+
if (target) {
|
|
811
|
+
filePaths.add(path.isAbsolute(target) ? target : path.resolve(cwd, target));
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
const snapshots = [];
|
|
815
|
+
for (const absPath of filePaths) {
|
|
816
|
+
const rel = path.relative(cwd, absPath).replace(/\\/g, "/");
|
|
817
|
+
const gitResult = sh(`git show HEAD:"${rel}"`, cwd);
|
|
818
|
+
const contentAfter = fs.existsSync(absPath)
|
|
819
|
+
? fs.readFileSync(absPath, "utf8")
|
|
820
|
+
: "";
|
|
821
|
+
if (!gitResult.ok) {
|
|
822
|
+
// File was not tracked at HEAD — created by Sela or untracked.
|
|
823
|
+
snapshots.push({
|
|
824
|
+
absolutePath: absPath,
|
|
825
|
+
relativePath: rel,
|
|
826
|
+
contentBefore: "",
|
|
827
|
+
contentAfter,
|
|
828
|
+
headContent: null,
|
|
829
|
+
hadUncommittedChanges: false,
|
|
830
|
+
createdBySela: true,
|
|
831
|
+
});
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
const headContent = gitResult.stdout;
|
|
835
|
+
snapshots.push({
|
|
836
|
+
absolutePath: absPath,
|
|
837
|
+
relativePath: rel,
|
|
838
|
+
contentBefore: headContent,
|
|
839
|
+
contentAfter,
|
|
840
|
+
headContent,
|
|
841
|
+
hadUncommittedChanges: false,
|
|
842
|
+
createdBySela: false,
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
// Only return files where Sela actually changed something.
|
|
846
|
+
return snapshots.filter((s) => s.contentAfter !== null && s.contentAfter !== s.contentBefore);
|
|
847
|
+
}
|
|
749
848
|
// ═══════════════════════════════════════════════════════════════════
|
|
750
849
|
// SHELL QUOTING - cross-platform (POSIX single-quote escape;
|
|
751
850
|
// on Windows execSync uses cmd.exe which handles double-quotes natively).
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import type { WorkspaceSnapshotService } from "./WorkspaceSnapshotService";
|
|
2
|
-
import type { HealedEvent } from "./HealReportService";
|
|
2
|
+
import type { HealedEvent, ProtectedEvent } from "./HealReportService";
|
|
3
|
+
/**
|
|
4
|
+
* The subset of worker state that the master process needs to run the
|
|
5
|
+
* centralized git/PR flow in SelaReporter.onEnd(). Written by every worker
|
|
6
|
+
* alongside the local-DX PendingPromptEntry list so the master can aggregate
|
|
7
|
+
* all workers' mutations and fire a single `gh pr create` after every worker
|
|
8
|
+
* has exited — without any FileLock contention.
|
|
9
|
+
*/
|
|
10
|
+
export interface WorkerGitPayload {
|
|
11
|
+
/** Absolute paths of every source file this worker mutated. */
|
|
12
|
+
mutatedFiles: string[];
|
|
13
|
+
healedEvents: HealedEvent[];
|
|
14
|
+
protectedEvents: ProtectedEvent[];
|
|
15
|
+
}
|
|
3
16
|
export interface PendingPromptEntry {
|
|
4
17
|
absolutePath: string;
|
|
5
18
|
relativePath: string;
|
|
@@ -36,16 +49,34 @@ export declare function writePendingPromptsLedger(opts: {
|
|
|
36
49
|
cwd: string;
|
|
37
50
|
workspace: WorkspaceSnapshotService;
|
|
38
51
|
healedEvents: HealedEvent[];
|
|
52
|
+
protectedEvents?: ProtectedEvent[];
|
|
39
53
|
}): string | null;
|
|
40
54
|
/**
|
|
41
55
|
* Drain every per-worker JSON file under the ledger directory and merge
|
|
42
56
|
* their entries into a single de-duplicated list (keyed by absolutePath).
|
|
43
57
|
* The most-recent contentAfter wins on collision - that's the disk truth.
|
|
44
58
|
*
|
|
59
|
+
* Handles both v1 (bare PendingPromptEntry[]) and v2 (LedgerFileV2) files.
|
|
60
|
+
*
|
|
45
61
|
* The directory is NOT deleted here so the reporter can inspect entries
|
|
46
62
|
* before calling `clearPendingPromptsLedger()`.
|
|
47
63
|
*/
|
|
48
64
|
export declare function readPendingPromptsLedger(cwd: string): PendingPromptEntry[];
|
|
65
|
+
/**
|
|
66
|
+
* Read the git-flow payloads written by each worker. Used by the master
|
|
67
|
+
* process (SelaReporter.onEnd) to aggregate all workers' mutations and
|
|
68
|
+
* fire a single centralized PR creation after all workers have exited.
|
|
69
|
+
*
|
|
70
|
+
* Only v2 ledger files carry a `git` payload; v1 files are skipped silently.
|
|
71
|
+
*/
|
|
72
|
+
export declare function readGitPayloads(cwd: string): WorkerGitPayload[];
|
|
73
|
+
/**
|
|
74
|
+
* Same as readGitPayloads but scans an arbitrary directory directly.
|
|
75
|
+
* Used by `sela pr aggregate --ledger-dir <path>` where the CI fan-in
|
|
76
|
+
* job downloads shard ledger artifacts into a flat directory rather than
|
|
77
|
+
* the standard `.sela/pending-prompts` subpath.
|
|
78
|
+
*/
|
|
79
|
+
export declare function readGitPayloadsFromDir(dir: string): WorkerGitPayload[];
|
|
49
80
|
/** Remove the ledger directory and every file beneath it. Idempotent. */
|
|
50
81
|
export declare function clearPendingPromptsLedger(cwd: string): void;
|
|
51
82
|
//# sourceMappingURL=PendingPromptLedger.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PendingPromptLedger.d.ts","sourceRoot":"","sources":["../../src/services/PendingPromptLedger.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAEV,wBAAwB,EACzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"PendingPromptLedger.d.ts","sourceRoot":"","sources":["../../src/services/PendingPromptLedger.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAEV,wBAAwB,EACzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAMvE;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+DAA+D;IAC/D,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,eAAe,EAAE,cAAc,EAAE,CAAC;CACnC;AAcD,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,OAAO,CAAC;IACvB,4EAA4E;IAC5E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;CACnB;AAID;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,wBAAwB,CAAC;IACpC,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;CACpC,GAAG,MAAM,GAAG,IAAI,CA8BhB;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAgC1E;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAmBtE;AAED,yEAAyE;AACzE,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAI3D"}
|
|
@@ -55,6 +55,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
55
55
|
exports.ledgerDir = ledgerDir;
|
|
56
56
|
exports.writePendingPromptsLedger = writePendingPromptsLedger;
|
|
57
57
|
exports.readPendingPromptsLedger = readPendingPromptsLedger;
|
|
58
|
+
exports.readGitPayloads = readGitPayloads;
|
|
59
|
+
exports.readGitPayloadsFromDir = readGitPayloadsFromDir;
|
|
58
60
|
exports.clearPendingPromptsLedger = clearPendingPromptsLedger;
|
|
59
61
|
const fs = __importStar(require("fs"));
|
|
60
62
|
const path = __importStar(require("path"));
|
|
@@ -78,15 +80,26 @@ function ledgerDir(cwd) {
|
|
|
78
80
|
* empty-mutation runs).
|
|
79
81
|
*/
|
|
80
82
|
function writePendingPromptsLedger(opts) {
|
|
83
|
+
const protectedEvents = opts.protectedEvents ?? [];
|
|
81
84
|
const snaps = opts.workspace.getMutatedSnapshots();
|
|
82
|
-
if (snaps.length === 0)
|
|
83
|
-
return null;
|
|
84
85
|
const entries = snaps.map((s) => snapshotToEntry(s, opts.healedEvents, opts.cwd));
|
|
86
|
+
const git = {
|
|
87
|
+
mutatedFiles: snaps.map((s) => s.absolutePath),
|
|
88
|
+
healedEvents: opts.healedEvents,
|
|
89
|
+
protectedEvents,
|
|
90
|
+
};
|
|
91
|
+
// Nothing to persist when there are no mutations AND no healed/protected events.
|
|
92
|
+
if (snaps.length === 0 && opts.healedEvents.length === 0 && protectedEvents.length === 0) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
85
95
|
const dir = ledgerDir(opts.cwd);
|
|
86
96
|
fs.mkdirSync(dir, { recursive: true });
|
|
87
|
-
|
|
97
|
+
// Filename includes workerIndex so concurrent workers never collide.
|
|
98
|
+
const workerIdx = process.env["TEST_WORKER_INDEX"] ?? "0";
|
|
99
|
+
const fileName = `${workerIdx}-${process.pid}-${Date.now()}-${randomTag()}.json`;
|
|
88
100
|
const full = path.join(dir, fileName);
|
|
89
|
-
|
|
101
|
+
const payload = { version: 2, entries, git };
|
|
102
|
+
fs.writeFileSync(full, JSON.stringify(payload, null, 2), "utf8");
|
|
90
103
|
return full;
|
|
91
104
|
}
|
|
92
105
|
/**
|
|
@@ -94,6 +107,8 @@ function writePendingPromptsLedger(opts) {
|
|
|
94
107
|
* their entries into a single de-duplicated list (keyed by absolutePath).
|
|
95
108
|
* The most-recent contentAfter wins on collision - that's the disk truth.
|
|
96
109
|
*
|
|
110
|
+
* Handles both v1 (bare PendingPromptEntry[]) and v2 (LedgerFileV2) files.
|
|
111
|
+
*
|
|
97
112
|
* The directory is NOT deleted here so the reporter can inspect entries
|
|
98
113
|
* before calling `clearPendingPromptsLedger()`.
|
|
99
114
|
*/
|
|
@@ -105,18 +120,24 @@ function readPendingPromptsLedger(cwd) {
|
|
|
105
120
|
const files = fs
|
|
106
121
|
.readdirSync(dir)
|
|
107
122
|
.filter((f) => f.endsWith(".json"))
|
|
108
|
-
.sort(); // pid-timestamp prefix gives a stable, time-ordered merge
|
|
123
|
+
.sort(); // workerIdx-pid-timestamp prefix gives a stable, time-ordered merge
|
|
109
124
|
for (const f of files) {
|
|
110
125
|
const full = path.join(dir, f);
|
|
111
126
|
try {
|
|
112
127
|
const raw = fs.readFileSync(full, "utf8");
|
|
113
128
|
const parsed = JSON.parse(raw);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
129
|
+
// v2: { version: 2, entries: [...], git: {...} }
|
|
130
|
+
// v1: bare PendingPromptEntry[]
|
|
131
|
+
const entryList = Array.isArray(parsed)
|
|
132
|
+
? parsed
|
|
133
|
+
: parsed.version === 2
|
|
134
|
+
? parsed.entries
|
|
135
|
+
: [];
|
|
136
|
+
for (const entry of entryList) {
|
|
137
|
+
const e = entry;
|
|
138
|
+
if (!e || typeof e.absolutePath !== "string")
|
|
118
139
|
continue;
|
|
119
|
-
merged.set(path.normalize(
|
|
140
|
+
merged.set(path.normalize(e.absolutePath), e);
|
|
120
141
|
}
|
|
121
142
|
}
|
|
122
143
|
catch {
|
|
@@ -125,6 +146,42 @@ function readPendingPromptsLedger(cwd) {
|
|
|
125
146
|
}
|
|
126
147
|
return Array.from(merged.values());
|
|
127
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Read the git-flow payloads written by each worker. Used by the master
|
|
151
|
+
* process (SelaReporter.onEnd) to aggregate all workers' mutations and
|
|
152
|
+
* fire a single centralized PR creation after all workers have exited.
|
|
153
|
+
*
|
|
154
|
+
* Only v2 ledger files carry a `git` payload; v1 files are skipped silently.
|
|
155
|
+
*/
|
|
156
|
+
function readGitPayloads(cwd) {
|
|
157
|
+
return readGitPayloadsFromDir(ledgerDir(cwd));
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Same as readGitPayloads but scans an arbitrary directory directly.
|
|
161
|
+
* Used by `sela pr aggregate --ledger-dir <path>` where the CI fan-in
|
|
162
|
+
* job downloads shard ledger artifacts into a flat directory rather than
|
|
163
|
+
* the standard `.sela/pending-prompts` subpath.
|
|
164
|
+
*/
|
|
165
|
+
function readGitPayloadsFromDir(dir) {
|
|
166
|
+
if (!fs.existsSync(dir))
|
|
167
|
+
return [];
|
|
168
|
+
const payloads = [];
|
|
169
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
170
|
+
for (const f of files) {
|
|
171
|
+
const full = path.join(dir, f);
|
|
172
|
+
try {
|
|
173
|
+
const raw = fs.readFileSync(full, "utf8");
|
|
174
|
+
const parsed = JSON.parse(raw);
|
|
175
|
+
if (parsed?.version === 2 && parsed.git) {
|
|
176
|
+
payloads.push(parsed.git);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Corrupt file - skip.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return payloads;
|
|
184
|
+
}
|
|
128
185
|
/** Remove the ledger directory and every file beneath it. Idempotent. */
|
|
129
186
|
function clearPendingPromptsLedger(cwd) {
|
|
130
187
|
const dir = ledgerDir(cwd);
|
package/package.json
CHANGED