@junctionpanel/server 0.1.21 → 0.1.23
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/server/client/daemon-client.d.ts +7 -1
- package/dist/server/client/daemon-client.d.ts.map +1 -1
- package/dist/server/client/daemon-client.js +23 -0
- package/dist/server/client/daemon-client.js.map +1 -1
- package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
- package/dist/server/server/agent/agent-manager.js +77 -0
- package/dist/server/server/agent/agent-manager.js.map +1 -1
- package/dist/server/server/session.d.ts +2 -0
- package/dist/server/server/session.d.ts.map +1 -1
- package/dist/server/server/session.js +71 -1
- package/dist/server/server/session.js.map +1 -1
- package/dist/server/shared/messages.d.ts +1008 -0
- package/dist/server/shared/messages.d.ts.map +1 -1
- package/dist/server/shared/messages.js +45 -0
- package/dist/server/shared/messages.js.map +1 -1
- package/dist/server/utils/checkout-git.d.ts +18 -0
- package/dist/server/utils/checkout-git.d.ts.map +1 -1
- package/dist/server/utils/checkout-git.js +533 -36
- package/dist/server/utils/checkout-git.js.map +1 -1
- package/package.json +2 -2
|
@@ -379,6 +379,19 @@ async function getWorktreeRoot(cwd) {
|
|
|
379
379
|
return null;
|
|
380
380
|
}
|
|
381
381
|
}
|
|
382
|
+
async function getUpstreamBranch(cwd) {
|
|
383
|
+
try {
|
|
384
|
+
const { stdout } = await execAsync("git rev-parse --abbrev-ref --symbolic-full-name @{upstream}", {
|
|
385
|
+
cwd,
|
|
386
|
+
env: READ_ONLY_GIT_ENV,
|
|
387
|
+
});
|
|
388
|
+
const upstream = stdout.trim();
|
|
389
|
+
return upstream.length > 0 ? upstream : null;
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
382
395
|
async function getMainRepoRoot(cwd) {
|
|
383
396
|
const { stdout: commonDirOut } = await execAsync("git rev-parse --path-format=absolute --git-common-dir", { cwd, env: READ_ONLY_GIT_ENV });
|
|
384
397
|
const commonDir = commonDirOut.trim();
|
|
@@ -738,11 +751,13 @@ export async function getCheckoutStatus(cwd, context) {
|
|
|
738
751
|
const isDirty = await isWorkingTreeDirty(cwd);
|
|
739
752
|
const hasRemote = remoteUrl !== null;
|
|
740
753
|
const baseRef = configured.baseRef ?? (await resolveBaseRef(cwd));
|
|
754
|
+
const upstreamBranch = currentBranch ? await getUpstreamBranch(cwd) : null;
|
|
741
755
|
const [aheadBehind, aheadOfOrigin, behindOfOrigin] = await Promise.all([
|
|
742
756
|
baseRef && currentBranch ? getAheadBehind(cwd, baseRef, currentBranch) : Promise.resolve(null),
|
|
743
757
|
hasRemote && currentBranch ? getAheadOfOrigin(cwd, currentBranch) : Promise.resolve(null),
|
|
744
758
|
hasRemote && currentBranch ? getBehindOfOrigin(cwd, currentBranch) : Promise.resolve(null),
|
|
745
759
|
]);
|
|
760
|
+
const hasUpstream = upstreamBranch !== null;
|
|
746
761
|
if (configured.isJunctionOwnedWorktree) {
|
|
747
762
|
const mainRepoRoot = await getMainRepoRoot(cwd);
|
|
748
763
|
return {
|
|
@@ -755,6 +770,8 @@ export async function getCheckoutStatus(cwd, context) {
|
|
|
755
770
|
aheadBehind,
|
|
756
771
|
aheadOfOrigin,
|
|
757
772
|
behindOfOrigin,
|
|
773
|
+
hasUpstream,
|
|
774
|
+
upstreamBranch,
|
|
758
775
|
hasRemote,
|
|
759
776
|
remoteUrl,
|
|
760
777
|
isJunctionOwnedWorktree: true,
|
|
@@ -769,6 +786,8 @@ export async function getCheckoutStatus(cwd, context) {
|
|
|
769
786
|
aheadBehind,
|
|
770
787
|
aheadOfOrigin,
|
|
771
788
|
behindOfOrigin,
|
|
789
|
+
hasUpstream,
|
|
790
|
+
upstreamBranch,
|
|
772
791
|
hasRemote,
|
|
773
792
|
remoteUrl,
|
|
774
793
|
isJunctionOwnedWorktree: false,
|
|
@@ -1200,6 +1219,9 @@ export async function pushCurrentBranch(cwd) {
|
|
|
1200
1219
|
}
|
|
1201
1220
|
await execAsync(`git push -u origin ${currentBranch}`, { cwd });
|
|
1202
1221
|
}
|
|
1222
|
+
const GH_JSON_MAX_BYTES = 512 * 1024;
|
|
1223
|
+
const GH_FAILED_LOG_MAX_BYTES = 200 * 1024;
|
|
1224
|
+
const GH_FAILED_LOG_TOTAL_MAX_BYTES = 300 * 1024;
|
|
1203
1225
|
async function ensureGhAvailable(cwd) {
|
|
1204
1226
|
try {
|
|
1205
1227
|
await execAsync("gh --version", { cwd });
|
|
@@ -1282,13 +1304,18 @@ async function listPullRequestsForHead(options) {
|
|
|
1282
1304
|
const parsed = JSON.parse(stdout.trim());
|
|
1283
1305
|
return Array.isArray(parsed) ? parsed : [];
|
|
1284
1306
|
}
|
|
1285
|
-
function
|
|
1307
|
+
function buildPullRequestBaseStatus(current, fallbackHead) {
|
|
1286
1308
|
if (!current || typeof current !== "object") {
|
|
1287
1309
|
return null;
|
|
1288
1310
|
}
|
|
1311
|
+
const number = typeof current.number === "number" && Number.isFinite(current.number) ? current.number : null;
|
|
1289
1312
|
const url = current.html_url ?? current.url;
|
|
1290
1313
|
const title = current.title;
|
|
1291
|
-
if (
|
|
1314
|
+
if (number === null ||
|
|
1315
|
+
typeof url !== "string" ||
|
|
1316
|
+
typeof title !== "string" ||
|
|
1317
|
+
!url ||
|
|
1318
|
+
!title) {
|
|
1292
1319
|
return null;
|
|
1293
1320
|
}
|
|
1294
1321
|
const mergedAt = typeof current.merged_at === "string" && current.merged_at.trim().length > 0
|
|
@@ -1300,49 +1327,346 @@ function buildPullRequestStatus(current, fallbackHead) {
|
|
|
1300
1327
|
? current.state
|
|
1301
1328
|
: "";
|
|
1302
1329
|
return {
|
|
1330
|
+
number,
|
|
1303
1331
|
url,
|
|
1304
1332
|
title,
|
|
1305
1333
|
state,
|
|
1306
1334
|
baseRefName: current.base?.ref ?? "",
|
|
1307
1335
|
headRefName: current.head?.ref ?? fallbackHead,
|
|
1308
1336
|
isMerged: mergedAt !== null,
|
|
1337
|
+
draft: Boolean(current.draft),
|
|
1338
|
+
headSha: typeof current.head?.sha === "string" ? current.head.sha : null,
|
|
1309
1339
|
};
|
|
1310
1340
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1341
|
+
function normalizeCheckBucket(value, state) {
|
|
1342
|
+
if (value === "pass" || value === "fail" || value === "pending" || value === "skipping" || value === "cancel") {
|
|
1343
|
+
return value;
|
|
1344
|
+
}
|
|
1345
|
+
const normalizedState = state.toLowerCase();
|
|
1346
|
+
if (normalizedState === "success" ||
|
|
1347
|
+
normalizedState === "neutral" ||
|
|
1348
|
+
normalizedState === "skipped") {
|
|
1349
|
+
return "pass";
|
|
1350
|
+
}
|
|
1351
|
+
if (normalizedState === "failure" ||
|
|
1352
|
+
normalizedState === "error" ||
|
|
1353
|
+
normalizedState === "timed_out" ||
|
|
1354
|
+
normalizedState === "startup_failure" ||
|
|
1355
|
+
normalizedState === "action_required") {
|
|
1356
|
+
return "fail";
|
|
1357
|
+
}
|
|
1358
|
+
if (normalizedState === "cancelled") {
|
|
1359
|
+
return "cancel";
|
|
1360
|
+
}
|
|
1361
|
+
if (normalizedState === "queued" ||
|
|
1362
|
+
normalizedState === "waiting" ||
|
|
1363
|
+
normalizedState === "requested" ||
|
|
1364
|
+
normalizedState === "pending" ||
|
|
1365
|
+
normalizedState === "in_progress") {
|
|
1366
|
+
return "pending";
|
|
1317
1367
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
if (!
|
|
1322
|
-
|
|
1368
|
+
return null;
|
|
1369
|
+
}
|
|
1370
|
+
function parsePullRequestChecks(input) {
|
|
1371
|
+
if (!Array.isArray(input)) {
|
|
1372
|
+
return [];
|
|
1323
1373
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1374
|
+
return input
|
|
1375
|
+
.map((entry) => {
|
|
1376
|
+
if (!entry || typeof entry !== "object") {
|
|
1377
|
+
return null;
|
|
1378
|
+
}
|
|
1379
|
+
const state = typeof entry.state === "string" ? entry.state : "";
|
|
1380
|
+
const bucket = normalizeCheckBucket(entry.bucket, state);
|
|
1381
|
+
const name = typeof entry.name === "string" ? entry.name.trim() : "";
|
|
1382
|
+
if (!bucket || !name) {
|
|
1383
|
+
return null;
|
|
1384
|
+
}
|
|
1385
|
+
return {
|
|
1386
|
+
name,
|
|
1387
|
+
workflow: typeof entry.workflow === "string" && entry.workflow.trim().length > 0
|
|
1388
|
+
? entry.workflow.trim()
|
|
1389
|
+
: null,
|
|
1390
|
+
link: typeof entry.link === "string" && entry.link.trim().length > 0 ? entry.link : null,
|
|
1391
|
+
description: typeof entry.description === "string" && entry.description.trim().length > 0
|
|
1392
|
+
? entry.description.trim()
|
|
1393
|
+
: null,
|
|
1394
|
+
state,
|
|
1395
|
+
bucket,
|
|
1396
|
+
};
|
|
1397
|
+
})
|
|
1398
|
+
.filter((entry) => entry !== null);
|
|
1399
|
+
}
|
|
1400
|
+
async function getRequiredPullRequestChecks(cwd, prNumber) {
|
|
1401
|
+
let result;
|
|
1402
|
+
try {
|
|
1403
|
+
result = await spawnLimitedText({
|
|
1404
|
+
cmd: "gh",
|
|
1405
|
+
args: [
|
|
1406
|
+
"pr",
|
|
1407
|
+
"checks",
|
|
1408
|
+
String(prNumber),
|
|
1409
|
+
"--required",
|
|
1410
|
+
"--json",
|
|
1411
|
+
"bucket,description,link,name,state,workflow",
|
|
1412
|
+
],
|
|
1413
|
+
cwd,
|
|
1414
|
+
maxBytes: GH_JSON_MAX_BYTES,
|
|
1415
|
+
acceptExitCodes: [0, 1, 8],
|
|
1416
|
+
});
|
|
1326
1417
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1418
|
+
catch (error) {
|
|
1419
|
+
if (getCommandErrorText(error).includes("no checks reported")) {
|
|
1420
|
+
return [];
|
|
1421
|
+
}
|
|
1422
|
+
throw error;
|
|
1330
1423
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
args.push("-f", `base=${normalizedBase}`);
|
|
1335
|
-
if (options.body) {
|
|
1336
|
-
args.push("-f", `body=${options.body}`);
|
|
1424
|
+
const text = result.text.trim();
|
|
1425
|
+
if (!text) {
|
|
1426
|
+
return [];
|
|
1337
1427
|
}
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1428
|
+
return parsePullRequestChecks(JSON.parse(text));
|
|
1429
|
+
}
|
|
1430
|
+
function parseStatusCheckRollup(input) {
|
|
1431
|
+
if (!Array.isArray(input)) {
|
|
1432
|
+
return [];
|
|
1342
1433
|
}
|
|
1343
|
-
return
|
|
1434
|
+
return input
|
|
1435
|
+
.map((entry) => {
|
|
1436
|
+
if (!entry || typeof entry !== "object") {
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
const statusValue = typeof entry.status === "string" && entry.status.trim().length > 0
|
|
1440
|
+
? entry.status.trim()
|
|
1441
|
+
: typeof entry.state === "string" && entry.state.trim().length > 0
|
|
1442
|
+
? entry.state.trim()
|
|
1443
|
+
: "";
|
|
1444
|
+
const conclusionValue = typeof entry.conclusion === "string" && entry.conclusion.trim().length > 0
|
|
1445
|
+
? entry.conclusion.trim()
|
|
1446
|
+
: "";
|
|
1447
|
+
const bucket = normalizeCheckBucket(undefined, conclusionValue || statusValue) ??
|
|
1448
|
+
(statusValue.toLowerCase() === "completed" && conclusionValue
|
|
1449
|
+
? normalizeCheckBucket(undefined, conclusionValue)
|
|
1450
|
+
: null);
|
|
1451
|
+
const name = typeof entry.name === "string" && entry.name.trim().length > 0
|
|
1452
|
+
? entry.name.trim()
|
|
1453
|
+
: typeof entry.context === "string" && entry.context.trim().length > 0
|
|
1454
|
+
? entry.context.trim()
|
|
1455
|
+
: "";
|
|
1456
|
+
if (!bucket || !name) {
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
1459
|
+
return {
|
|
1460
|
+
name,
|
|
1461
|
+
workflow: typeof entry.workflowName === "string" && entry.workflowName.trim().length > 0
|
|
1462
|
+
? entry.workflowName.trim()
|
|
1463
|
+
: null,
|
|
1464
|
+
link: typeof entry.detailsUrl === "string" && entry.detailsUrl.trim().length > 0
|
|
1465
|
+
? entry.detailsUrl.trim()
|
|
1466
|
+
: typeof entry.targetUrl === "string" && entry.targetUrl.trim().length > 0
|
|
1467
|
+
? entry.targetUrl.trim()
|
|
1468
|
+
: null,
|
|
1469
|
+
description: typeof entry.description === "string" && entry.description.trim().length > 0
|
|
1470
|
+
? entry.description.trim()
|
|
1471
|
+
: null,
|
|
1472
|
+
state: conclusionValue || statusValue,
|
|
1473
|
+
bucket,
|
|
1474
|
+
};
|
|
1475
|
+
})
|
|
1476
|
+
.filter((entry) => entry !== null);
|
|
1344
1477
|
}
|
|
1345
|
-
|
|
1478
|
+
function mergeCheckStatuses(requiredChecks, rollupChecks) {
|
|
1479
|
+
const merged = new Map();
|
|
1480
|
+
for (const check of rollupChecks) {
|
|
1481
|
+
const key = `${check.workflow ?? ""}::${check.name}`;
|
|
1482
|
+
merged.set(key, check);
|
|
1483
|
+
}
|
|
1484
|
+
for (const check of requiredChecks) {
|
|
1485
|
+
const key = `${check.workflow ?? ""}::${check.name}`;
|
|
1486
|
+
merged.set(key, check);
|
|
1487
|
+
}
|
|
1488
|
+
return Array.from(merged.values());
|
|
1489
|
+
}
|
|
1490
|
+
function deriveChecksState(checks) {
|
|
1491
|
+
if (checks.some((check) => check.bucket === "fail" || check.bucket === "cancel")) {
|
|
1492
|
+
return "failing";
|
|
1493
|
+
}
|
|
1494
|
+
if (checks.some((check) => check.bucket === "pending")) {
|
|
1495
|
+
return "pending";
|
|
1496
|
+
}
|
|
1497
|
+
return "passing";
|
|
1498
|
+
}
|
|
1499
|
+
async function getPullRequestMergeability(cwd, prNumber) {
|
|
1500
|
+
const result = await spawnLimitedText({
|
|
1501
|
+
cmd: "gh",
|
|
1502
|
+
args: [
|
|
1503
|
+
"pr",
|
|
1504
|
+
"view",
|
|
1505
|
+
String(prNumber),
|
|
1506
|
+
"--json",
|
|
1507
|
+
"mergeable,mergeStateStatus,statusCheckRollup",
|
|
1508
|
+
],
|
|
1509
|
+
cwd,
|
|
1510
|
+
maxBytes: GH_JSON_MAX_BYTES,
|
|
1511
|
+
});
|
|
1512
|
+
const text = result.text.trim();
|
|
1513
|
+
if (!text) {
|
|
1514
|
+
return { hasConflicts: false, rollupChecks: [] };
|
|
1515
|
+
}
|
|
1516
|
+
const parsed = JSON.parse(text);
|
|
1517
|
+
const mergeable = typeof parsed.mergeable === "string" ? parsed.mergeable.trim().toUpperCase() : "";
|
|
1518
|
+
const mergeStateStatus = typeof parsed.mergeStateStatus === "string"
|
|
1519
|
+
? parsed.mergeStateStatus.trim().toUpperCase()
|
|
1520
|
+
: "";
|
|
1521
|
+
return {
|
|
1522
|
+
hasConflicts: mergeable === "CONFLICTING" || mergeStateStatus === "DIRTY",
|
|
1523
|
+
rollupChecks: parseStatusCheckRollup(parsed.statusCheckRollup),
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
function toPullRequestStatus(baseStatus, checks, mergeability) {
|
|
1527
|
+
const checksState = baseStatus.isMerged ? "passing" : deriveChecksState(checks);
|
|
1528
|
+
const requiredChecksPassed = baseStatus.isMerged
|
|
1529
|
+
? true
|
|
1530
|
+
: checks.every((check) => check.bucket === "pass" || check.bucket === "skipping");
|
|
1531
|
+
const hasConflicts = baseStatus.isMerged ? false : (mergeability?.hasConflicts ?? false);
|
|
1532
|
+
return {
|
|
1533
|
+
number: baseStatus.number,
|
|
1534
|
+
url: baseStatus.url,
|
|
1535
|
+
title: baseStatus.title,
|
|
1536
|
+
state: baseStatus.state,
|
|
1537
|
+
baseRefName: baseStatus.baseRefName,
|
|
1538
|
+
headRefName: baseStatus.headRefName,
|
|
1539
|
+
isMerged: baseStatus.isMerged,
|
|
1540
|
+
checksState,
|
|
1541
|
+
requiredChecksPassed,
|
|
1542
|
+
canMerge: !baseStatus.isMerged && !baseStatus.draft && requiredChecksPassed && !hasConflicts,
|
|
1543
|
+
hasConflicts,
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
async function listWorkflowRunsForHead(options) {
|
|
1547
|
+
const args = [
|
|
1548
|
+
"run",
|
|
1549
|
+
"list",
|
|
1550
|
+
"--json",
|
|
1551
|
+
"conclusion,createdAt,databaseId,displayTitle,name,status,url,workflowName",
|
|
1552
|
+
"-L",
|
|
1553
|
+
"50",
|
|
1554
|
+
];
|
|
1555
|
+
if (options.headSha) {
|
|
1556
|
+
args.push("--commit", options.headSha);
|
|
1557
|
+
}
|
|
1558
|
+
else {
|
|
1559
|
+
args.push("--branch", options.headBranch);
|
|
1560
|
+
}
|
|
1561
|
+
const result = await spawnLimitedText({
|
|
1562
|
+
cmd: "gh",
|
|
1563
|
+
args,
|
|
1564
|
+
cwd: options.cwd,
|
|
1565
|
+
maxBytes: GH_JSON_MAX_BYTES,
|
|
1566
|
+
});
|
|
1567
|
+
const text = result.text.trim();
|
|
1568
|
+
if (!text) {
|
|
1569
|
+
return [];
|
|
1570
|
+
}
|
|
1571
|
+
const parsed = JSON.parse(text);
|
|
1572
|
+
if (!Array.isArray(parsed)) {
|
|
1573
|
+
return [];
|
|
1574
|
+
}
|
|
1575
|
+
return parsed
|
|
1576
|
+
.map((entry) => {
|
|
1577
|
+
if (!entry || typeof entry !== "object") {
|
|
1578
|
+
return null;
|
|
1579
|
+
}
|
|
1580
|
+
const databaseId = typeof entry.databaseId === "number" && Number.isFinite(entry.databaseId)
|
|
1581
|
+
? entry.databaseId
|
|
1582
|
+
: null;
|
|
1583
|
+
if (databaseId === null) {
|
|
1584
|
+
return null;
|
|
1585
|
+
}
|
|
1586
|
+
return {
|
|
1587
|
+
databaseId,
|
|
1588
|
+
workflowName: typeof entry.workflowName === "string" && entry.workflowName.trim().length > 0
|
|
1589
|
+
? entry.workflowName.trim()
|
|
1590
|
+
: null,
|
|
1591
|
+
name: typeof entry.name === "string" && entry.name.trim().length > 0 ? entry.name.trim() : null,
|
|
1592
|
+
displayTitle: typeof entry.displayTitle === "string" && entry.displayTitle.trim().length > 0
|
|
1593
|
+
? entry.displayTitle.trim()
|
|
1594
|
+
: null,
|
|
1595
|
+
url: typeof entry.url === "string" && entry.url.trim().length > 0 ? entry.url.trim() : null,
|
|
1596
|
+
status: typeof entry.status === "string" && entry.status.trim().length > 0
|
|
1597
|
+
? entry.status.trim()
|
|
1598
|
+
: null,
|
|
1599
|
+
conclusion: typeof entry.conclusion === "string" && entry.conclusion.trim().length > 0
|
|
1600
|
+
? entry.conclusion.trim()
|
|
1601
|
+
: null,
|
|
1602
|
+
createdAt: typeof entry.createdAt === "string" && entry.createdAt.trim().length > 0
|
|
1603
|
+
? entry.createdAt.trim()
|
|
1604
|
+
: null,
|
|
1605
|
+
};
|
|
1606
|
+
})
|
|
1607
|
+
.filter((entry) => entry !== null);
|
|
1608
|
+
}
|
|
1609
|
+
function isWorkflowRunFailed(run) {
|
|
1610
|
+
const conclusion = run.conclusion?.toLowerCase() ?? "";
|
|
1611
|
+
return (conclusion === "failure" ||
|
|
1612
|
+
conclusion === "cancelled" ||
|
|
1613
|
+
conclusion === "timed_out" ||
|
|
1614
|
+
conclusion === "startup_failure" ||
|
|
1615
|
+
conclusion === "action_required");
|
|
1616
|
+
}
|
|
1617
|
+
function findRunsForFailingChecks(checks, runs) {
|
|
1618
|
+
const failedRuns = runs.filter(isWorkflowRunFailed);
|
|
1619
|
+
const selected = [];
|
|
1620
|
+
const seen = new Set();
|
|
1621
|
+
for (const check of checks) {
|
|
1622
|
+
if (check.bucket !== "fail" && check.bucket !== "cancel") {
|
|
1623
|
+
continue;
|
|
1624
|
+
}
|
|
1625
|
+
const matchingRun = failedRuns.find((run) => check.workflow !== null &&
|
|
1626
|
+
(run.workflowName === check.workflow || run.name === check.workflow)) ??
|
|
1627
|
+
failedRuns.find((run) => run.name === check.name || run.displayTitle === check.name) ??
|
|
1628
|
+
null;
|
|
1629
|
+
if (matchingRun && !seen.has(matchingRun.databaseId)) {
|
|
1630
|
+
seen.add(matchingRun.databaseId);
|
|
1631
|
+
selected.push(matchingRun);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
if (selected.length > 0) {
|
|
1635
|
+
return selected;
|
|
1636
|
+
}
|
|
1637
|
+
return failedRuns.slice(0, 3);
|
|
1638
|
+
}
|
|
1639
|
+
async function getFailedWorkflowRunLog(cwd, runId) {
|
|
1640
|
+
const result = await spawnLimitedText({
|
|
1641
|
+
cmd: "gh",
|
|
1642
|
+
args: ["run", "view", String(runId), "--log-failed"],
|
|
1643
|
+
cwd,
|
|
1644
|
+
maxBytes: GH_FAILED_LOG_MAX_BYTES,
|
|
1645
|
+
});
|
|
1646
|
+
const text = result.text.trim();
|
|
1647
|
+
if (!text) {
|
|
1648
|
+
return "No failed-step logs were returned by GitHub CLI.";
|
|
1649
|
+
}
|
|
1650
|
+
return result.truncated ? `${text}\n\n[log truncated]` : text;
|
|
1651
|
+
}
|
|
1652
|
+
function appendBoundedText(parts, next, currentBytes) {
|
|
1653
|
+
if (currentBytes >= GH_FAILED_LOG_TOTAL_MAX_BYTES) {
|
|
1654
|
+
return currentBytes;
|
|
1655
|
+
}
|
|
1656
|
+
const nextBytes = Buffer.byteLength(next, "utf8");
|
|
1657
|
+
if (currentBytes + nextBytes <= GH_FAILED_LOG_TOTAL_MAX_BYTES) {
|
|
1658
|
+
parts.push(next);
|
|
1659
|
+
return currentBytes + nextBytes;
|
|
1660
|
+
}
|
|
1661
|
+
const remaining = GH_FAILED_LOG_TOTAL_MAX_BYTES - currentBytes;
|
|
1662
|
+
if (remaining <= 0) {
|
|
1663
|
+
return currentBytes;
|
|
1664
|
+
}
|
|
1665
|
+
const truncated = Buffer.from(next, "utf8").subarray(0, remaining).toString("utf8");
|
|
1666
|
+
parts.push(`${truncated}\n\n[combined log output truncated]`);
|
|
1667
|
+
return GH_FAILED_LOG_TOTAL_MAX_BYTES;
|
|
1668
|
+
}
|
|
1669
|
+
async function loadCurrentPullRequest(cwd) {
|
|
1346
1670
|
await requireGitRepo(cwd);
|
|
1347
1671
|
const repo = await resolveGitHubRepo(cwd);
|
|
1348
1672
|
const head = await getCurrentBranch(cwd);
|
|
@@ -1350,6 +1674,8 @@ export async function getPullRequestStatus(cwd) {
|
|
|
1350
1674
|
return {
|
|
1351
1675
|
status: null,
|
|
1352
1676
|
githubFeaturesEnabled: false,
|
|
1677
|
+
checks: [],
|
|
1678
|
+
headSha: null,
|
|
1353
1679
|
};
|
|
1354
1680
|
}
|
|
1355
1681
|
try {
|
|
@@ -1359,6 +1685,8 @@ export async function getPullRequestStatus(cwd) {
|
|
|
1359
1685
|
return {
|
|
1360
1686
|
status: null,
|
|
1361
1687
|
githubFeaturesEnabled: false,
|
|
1688
|
+
checks: [],
|
|
1689
|
+
headSha: null,
|
|
1362
1690
|
};
|
|
1363
1691
|
}
|
|
1364
1692
|
const owner = repo.split("/")[0];
|
|
@@ -1377,16 +1705,49 @@ export async function getPullRequestStatus(cwd) {
|
|
|
1377
1705
|
return {
|
|
1378
1706
|
status: null,
|
|
1379
1707
|
githubFeaturesEnabled: false,
|
|
1708
|
+
checks: [],
|
|
1709
|
+
headSha: null,
|
|
1380
1710
|
};
|
|
1381
1711
|
}
|
|
1382
1712
|
throw error;
|
|
1383
1713
|
}
|
|
1384
1714
|
const openPull = openPulls[0] ?? null;
|
|
1385
|
-
const
|
|
1386
|
-
if (
|
|
1715
|
+
const openBaseStatus = buildPullRequestBaseStatus(openPull, head);
|
|
1716
|
+
if (openBaseStatus) {
|
|
1717
|
+
let checks;
|
|
1718
|
+
let mergeability = { hasConflicts: false, rollupChecks: [] };
|
|
1719
|
+
try {
|
|
1720
|
+
checks = await getRequiredPullRequestChecks(cwd, openBaseStatus.number);
|
|
1721
|
+
}
|
|
1722
|
+
catch (error) {
|
|
1723
|
+
if (isGhAuthError(error)) {
|
|
1724
|
+
return {
|
|
1725
|
+
status: null,
|
|
1726
|
+
githubFeaturesEnabled: false,
|
|
1727
|
+
checks: [],
|
|
1728
|
+
headSha: null,
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
throw error;
|
|
1732
|
+
}
|
|
1733
|
+
try {
|
|
1734
|
+
mergeability = await getPullRequestMergeability(cwd, openBaseStatus.number);
|
|
1735
|
+
}
|
|
1736
|
+
catch (error) {
|
|
1737
|
+
if (isGhAuthError(error)) {
|
|
1738
|
+
return {
|
|
1739
|
+
status: null,
|
|
1740
|
+
githubFeaturesEnabled: false,
|
|
1741
|
+
checks: [],
|
|
1742
|
+
headSha: null,
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1387
1746
|
return {
|
|
1388
|
-
status:
|
|
1747
|
+
status: toPullRequestStatus(openBaseStatus, mergeCheckStatuses(checks, mergeability.rollupChecks), mergeability),
|
|
1389
1748
|
githubFeaturesEnabled: true,
|
|
1749
|
+
checks: mergeCheckStatuses(checks, mergeability.rollupChecks),
|
|
1750
|
+
headSha: openBaseStatus.headSha,
|
|
1390
1751
|
};
|
|
1391
1752
|
}
|
|
1392
1753
|
let closedPulls;
|
|
@@ -1404,6 +1765,8 @@ export async function getPullRequestStatus(cwd) {
|
|
|
1404
1765
|
return {
|
|
1405
1766
|
status: null,
|
|
1406
1767
|
githubFeaturesEnabled: false,
|
|
1768
|
+
checks: [],
|
|
1769
|
+
headSha: null,
|
|
1407
1770
|
};
|
|
1408
1771
|
}
|
|
1409
1772
|
throw error;
|
|
@@ -1412,16 +1775,150 @@ export async function getPullRequestStatus(cwd) {
|
|
|
1412
1775
|
typeof entry === "object" &&
|
|
1413
1776
|
typeof entry.merged_at === "string" &&
|
|
1414
1777
|
entry.merged_at.trim().length > 0) ?? null;
|
|
1415
|
-
const
|
|
1416
|
-
if (!
|
|
1778
|
+
const mergedBaseStatus = buildPullRequestBaseStatus(mergedClosedPull, head);
|
|
1779
|
+
if (!mergedBaseStatus) {
|
|
1417
1780
|
return {
|
|
1418
1781
|
status: null,
|
|
1419
1782
|
githubFeaturesEnabled: true,
|
|
1783
|
+
checks: [],
|
|
1784
|
+
headSha: null,
|
|
1420
1785
|
};
|
|
1421
1786
|
}
|
|
1422
1787
|
return {
|
|
1423
|
-
status:
|
|
1788
|
+
status: toPullRequestStatus(mergedBaseStatus, []),
|
|
1424
1789
|
githubFeaturesEnabled: true,
|
|
1790
|
+
checks: [],
|
|
1791
|
+
headSha: mergedBaseStatus.headSha,
|
|
1425
1792
|
};
|
|
1426
1793
|
}
|
|
1794
|
+
export async function createPullRequest(cwd, options) {
|
|
1795
|
+
await requireGitRepo(cwd);
|
|
1796
|
+
await ensureGhAvailable(cwd);
|
|
1797
|
+
const repo = await resolveGitHubRepo(cwd);
|
|
1798
|
+
if (!repo) {
|
|
1799
|
+
throw new Error("Unable to determine GitHub repo from git remote");
|
|
1800
|
+
}
|
|
1801
|
+
const head = options.head ?? (await getCurrentBranch(cwd));
|
|
1802
|
+
const configured = await getConfiguredBaseRefForCwd(cwd);
|
|
1803
|
+
const base = configured.baseRef ?? options.base ?? (await resolveBaseRef(cwd));
|
|
1804
|
+
if (!head) {
|
|
1805
|
+
throw new Error("Unable to determine head branch for PR");
|
|
1806
|
+
}
|
|
1807
|
+
if (!base) {
|
|
1808
|
+
throw new Error("Unable to determine base branch for PR");
|
|
1809
|
+
}
|
|
1810
|
+
const normalizedBase = normalizeLocalBranchRefName(base);
|
|
1811
|
+
if (configured.isJunctionOwnedWorktree && options.base && options.base !== base) {
|
|
1812
|
+
throw new Error(`Base ref mismatch: expected ${base}, got ${options.base}`);
|
|
1813
|
+
}
|
|
1814
|
+
await execAsync(`git push -u origin ${head}`, { cwd });
|
|
1815
|
+
const args = ["api", "-X", "POST", `repos/${repo}/pulls`, "-f", `title=${options.title}`];
|
|
1816
|
+
args.push("-f", `head=${head}`);
|
|
1817
|
+
args.push("-f", `base=${normalizedBase}`);
|
|
1818
|
+
if (options.body) {
|
|
1819
|
+
args.push("-f", `body=${options.body}`);
|
|
1820
|
+
}
|
|
1821
|
+
const { stdout } = await execFileAsync("gh", args, { cwd });
|
|
1822
|
+
const parsed = JSON.parse(stdout.trim());
|
|
1823
|
+
if (!parsed?.url || !parsed?.number) {
|
|
1824
|
+
throw new Error("GitHub CLI did not return PR url/number");
|
|
1825
|
+
}
|
|
1826
|
+
return { url: parsed.url, number: parsed.number };
|
|
1827
|
+
}
|
|
1828
|
+
export async function getPullRequestStatus(cwd) {
|
|
1829
|
+
const lookup = await loadCurrentPullRequest(cwd);
|
|
1830
|
+
return {
|
|
1831
|
+
status: lookup.status,
|
|
1832
|
+
githubFeaturesEnabled: lookup.githubFeaturesEnabled,
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
export async function getPullRequestFailureLogs(cwd) {
|
|
1836
|
+
const lookup = await loadCurrentPullRequest(cwd);
|
|
1837
|
+
if (!lookup.githubFeaturesEnabled) {
|
|
1838
|
+
return {
|
|
1839
|
+
logs: null,
|
|
1840
|
+
githubFeaturesEnabled: false,
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
const failedChecks = lookup.checks.filter((check) => check.bucket === "fail" || check.bucket === "cancel");
|
|
1844
|
+
if (!lookup.status || lookup.status.isMerged || failedChecks.length === 0) {
|
|
1845
|
+
return {
|
|
1846
|
+
logs: null,
|
|
1847
|
+
githubFeaturesEnabled: true,
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
const runs = await listWorkflowRunsForHead({
|
|
1851
|
+
cwd,
|
|
1852
|
+
headBranch: lookup.status.headRefName,
|
|
1853
|
+
headSha: lookup.headSha,
|
|
1854
|
+
});
|
|
1855
|
+
const matchedRuns = findRunsForFailingChecks(lookup.checks, runs);
|
|
1856
|
+
const parts = [];
|
|
1857
|
+
let bytes = 0;
|
|
1858
|
+
bytes = appendBoundedText(parts, [
|
|
1859
|
+
`PR #${lookup.status.number}: ${lookup.status.title}`,
|
|
1860
|
+
`Branch: ${lookup.status.headRefName} -> ${lookup.status.baseRefName}`,
|
|
1861
|
+
"",
|
|
1862
|
+
"Failed required checks:",
|
|
1863
|
+
...failedChecks.map((check) => {
|
|
1864
|
+
const workflowSuffix = check.workflow ? ` [${check.workflow}]` : "";
|
|
1865
|
+
const descriptionSuffix = check.description ? ` - ${check.description}` : "";
|
|
1866
|
+
return `- ${check.name}${workflowSuffix}${descriptionSuffix}`;
|
|
1867
|
+
}),
|
|
1868
|
+
"",
|
|
1869
|
+
].join("\n"), bytes);
|
|
1870
|
+
if (matchedRuns.length === 0) {
|
|
1871
|
+
return {
|
|
1872
|
+
logs: parts.join(""),
|
|
1873
|
+
githubFeaturesEnabled: true,
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
for (const run of matchedRuns) {
|
|
1877
|
+
let body = "No failed-step logs were returned by GitHub CLI.";
|
|
1878
|
+
try {
|
|
1879
|
+
body = await getFailedWorkflowRunLog(cwd, run.databaseId);
|
|
1880
|
+
}
|
|
1881
|
+
catch (error) {
|
|
1882
|
+
if (isGhAuthError(error)) {
|
|
1883
|
+
return {
|
|
1884
|
+
logs: null,
|
|
1885
|
+
githubFeaturesEnabled: false,
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
body = error instanceof Error ? error.message : String(error);
|
|
1889
|
+
}
|
|
1890
|
+
const section = [
|
|
1891
|
+
`=== ${run.workflowName ?? run.name ?? `Run ${run.databaseId}`} ===`,
|
|
1892
|
+
run.url ? `URL: ${run.url}` : null,
|
|
1893
|
+
body,
|
|
1894
|
+
"",
|
|
1895
|
+
]
|
|
1896
|
+
.filter((value) => Boolean(value))
|
|
1897
|
+
.join("\n");
|
|
1898
|
+
bytes = appendBoundedText(parts, section, bytes);
|
|
1899
|
+
if (bytes >= GH_FAILED_LOG_TOTAL_MAX_BYTES) {
|
|
1900
|
+
break;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
return {
|
|
1904
|
+
logs: parts.join(""),
|
|
1905
|
+
githubFeaturesEnabled: true,
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
export async function mergePullRequest(cwd, options = {}) {
|
|
1909
|
+
const method = options.method ?? "squash";
|
|
1910
|
+
if (method !== "squash") {
|
|
1911
|
+
throw new Error("Only squash merge is supported");
|
|
1912
|
+
}
|
|
1913
|
+
const statusResult = await getPullRequestStatus(cwd);
|
|
1914
|
+
if (!statusResult.githubFeaturesEnabled) {
|
|
1915
|
+
throw new Error("GitHub CLI (gh) is not available or not authenticated");
|
|
1916
|
+
}
|
|
1917
|
+
if (!statusResult.status || statusResult.status.isMerged) {
|
|
1918
|
+
throw new Error("No open pull request found for current branch");
|
|
1919
|
+
}
|
|
1920
|
+
await execFileAsync("gh", ["pr", "merge", String(statusResult.status.number), "--squash"], {
|
|
1921
|
+
cwd,
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1427
1924
|
//# sourceMappingURL=checkout-git.js.map
|