@bensandee/forgejo-cli 0.5.5 → 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/bin.mjs +263 -183
- package/package.json +1 -1
package/dist/bin.mjs
CHANGED
|
@@ -162,7 +162,7 @@ function loadEnvFileForBin(rawArgs, bin, io = processIo) {
|
|
|
162
162
|
//#region src/version.ts
|
|
163
163
|
const parsed = z.object({ version: z.string() }).safeParse({
|
|
164
164
|
name: "@bensandee/forgejo-cli",
|
|
165
|
-
version: "0.
|
|
165
|
+
version: "0.6.0",
|
|
166
166
|
description: "bst-fj: Forgejo API commands without the rest of bst",
|
|
167
167
|
license: "MIT",
|
|
168
168
|
repository: {
|
|
@@ -643,6 +643,11 @@ function listFlagPresent(rawArgs, flag) {
|
|
|
643
643
|
const long = `--${flag}`;
|
|
644
644
|
return rawArgs.some((token) => token === long || token.startsWith(`${long}=`));
|
|
645
645
|
}
|
|
646
|
+
function parseLimitFlag(raw) {
|
|
647
|
+
const value = Number(raw);
|
|
648
|
+
if (!Number.isInteger(value) || value < 1) throw new FatalError(`--limit must be a positive integer, got "${raw}"`);
|
|
649
|
+
return value;
|
|
650
|
+
}
|
|
646
651
|
//#endregion
|
|
647
652
|
//#region ../cli-kit/src/output.ts
|
|
648
653
|
const stdout = (msg) => console.log(msg);
|
|
@@ -1073,20 +1078,23 @@ function cliFetcher() {
|
|
|
1073
1078
|
//#endregion
|
|
1074
1079
|
//#region ../forgejo/src/errors.ts
|
|
1075
1080
|
var ForgeAccessError = class extends FatalError {};
|
|
1076
|
-
function authHeaders
|
|
1081
|
+
function authHeaders(conn) {
|
|
1077
1082
|
return { Authorization: `token ${conn.token}` };
|
|
1078
1083
|
}
|
|
1079
1084
|
function jsonHeaders(conn) {
|
|
1080
1085
|
return {
|
|
1081
|
-
...authHeaders
|
|
1086
|
+
...authHeaders(conn),
|
|
1082
1087
|
"Content-Type": "application/json"
|
|
1083
1088
|
};
|
|
1084
1089
|
}
|
|
1085
1090
|
function repoNotFoundMessage(conn) {
|
|
1086
1091
|
return `Repository ${conn.repository} not found (or the token cannot see it)`;
|
|
1087
1092
|
}
|
|
1093
|
+
function repoBase(conn) {
|
|
1094
|
+
return `${conn.serverUrl}/api/v1/repos/${conn.repository}`;
|
|
1095
|
+
}
|
|
1088
1096
|
function issuesBase(conn) {
|
|
1089
|
-
return `${conn
|
|
1097
|
+
return `${repoBase(conn)}/issues`;
|
|
1090
1098
|
}
|
|
1091
1099
|
function issueNotFoundMessage(conn, number) {
|
|
1092
1100
|
return `Issue #${String(number)} not found in ${conn.repository}`;
|
|
@@ -1286,18 +1294,12 @@ z.object({
|
|
|
1286
1294
|
created: z.string()
|
|
1287
1295
|
});
|
|
1288
1296
|
z.array(z.unknown());
|
|
1289
|
-
function repoBase$1(conn) {
|
|
1290
|
-
return `${conn.serverUrl}/api/v1/repos/${conn.repository}`;
|
|
1291
|
-
}
|
|
1292
|
-
function authHeaders$1(conn) {
|
|
1293
|
-
return { Authorization: `token ${conn.token}` };
|
|
1294
|
-
}
|
|
1295
1297
|
function branchNotFoundMessage(conn, branch) {
|
|
1296
1298
|
return `Branch ${branch} not found in ${conn.repository}`;
|
|
1297
1299
|
}
|
|
1298
1300
|
async function getBranchHeadSha(executor, conn, branch) {
|
|
1299
|
-
const url = `${repoBase
|
|
1300
|
-
const res = await executor.fetch(url, { headers: authHeaders
|
|
1301
|
+
const url = `${repoBase(conn)}/branches/${encodeURIComponent(branch)}`;
|
|
1302
|
+
const res = await executor.fetch(url, { headers: authHeaders(conn) });
|
|
1301
1303
|
if (!res.ok) await throwForgejoFailure(res, {
|
|
1302
1304
|
verb: "read branch",
|
|
1303
1305
|
resource: branch
|
|
@@ -1307,8 +1309,8 @@ async function getBranchHeadSha(executor, conn, branch) {
|
|
|
1307
1309
|
return parsed.data.commit.id;
|
|
1308
1310
|
}
|
|
1309
1311
|
async function branchExists(executor, conn, branch) {
|
|
1310
|
-
const url = `${repoBase
|
|
1311
|
-
const res = await executor.fetch(url, { headers: authHeaders
|
|
1312
|
+
const url = `${repoBase(conn)}/branches/${encodeURIComponent(branch)}`;
|
|
1313
|
+
const res = await executor.fetch(url, { headers: authHeaders(conn) });
|
|
1312
1314
|
if (res.status === 404) return false;
|
|
1313
1315
|
if (!res.ok) await throwForgejoFailure(res, {
|
|
1314
1316
|
verb: "check branch",
|
|
@@ -1441,17 +1443,69 @@ async function readPollChecks(fetcher, conn, sha) {
|
|
|
1441
1443
|
return { failureDetail: describeError(error) };
|
|
1442
1444
|
}
|
|
1443
1445
|
}
|
|
1446
|
+
async function settleChecks(fetcher, conn, poll, round) {
|
|
1447
|
+
const { timeoutMs, intervalMs, sleep, now } = poll;
|
|
1448
|
+
const startedAt = now();
|
|
1449
|
+
const elapsed = () => now() - startedAt;
|
|
1450
|
+
let consecutiveFailures = 0;
|
|
1451
|
+
let lastFailureDetail;
|
|
1452
|
+
let lastGoodChecks;
|
|
1453
|
+
let previousSettledContexts = null;
|
|
1454
|
+
for (;;) {
|
|
1455
|
+
const sha = round.sha();
|
|
1456
|
+
const { checks, failureDetail } = await readPollChecks(fetcher, conn, sha);
|
|
1457
|
+
if (failureDetail !== void 0) lastFailureDetail = failureDetail;
|
|
1458
|
+
if (checks === void 0) {
|
|
1459
|
+
consecutiveFailures += 1;
|
|
1460
|
+
if (consecutiveFailures >= 3) return round.report(EMPTY_CHECKS, {
|
|
1461
|
+
exit: "retry-budget",
|
|
1462
|
+
elapsed,
|
|
1463
|
+
failureDetail: lastFailureDetail
|
|
1464
|
+
});
|
|
1465
|
+
} else {
|
|
1466
|
+
consecutiveFailures = 0;
|
|
1467
|
+
lastGoodChecks = checks;
|
|
1468
|
+
pushDisagreementNotice(round.notices, sha, checks);
|
|
1469
|
+
if (isSettled(checks)) {
|
|
1470
|
+
const contexts = new Set(checks.contexts.map((check) => check.context));
|
|
1471
|
+
if (previousSettledContexts !== null && previousSettledContexts.size === contexts.size && [...previousSettledContexts].every((context) => contexts.has(context))) return round.report(checks, {
|
|
1472
|
+
exit: "settled",
|
|
1473
|
+
elapsed
|
|
1474
|
+
});
|
|
1475
|
+
previousSettledContexts = contexts;
|
|
1476
|
+
} else previousSettledContexts = null;
|
|
1477
|
+
}
|
|
1478
|
+
if (elapsed() >= timeoutMs) return round.report(lastGoodChecks ?? EMPTY_CHECKS, {
|
|
1479
|
+
exit: "timeout",
|
|
1480
|
+
elapsed
|
|
1481
|
+
});
|
|
1482
|
+
await sleep(intervalMs);
|
|
1483
|
+
if (round.afterSleep !== void 0) {
|
|
1484
|
+
const after = await round.afterSleep(sha, elapsed);
|
|
1485
|
+
if (after !== "retry" && after !== "next") return after.done;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
function renderCheckLine(check) {
|
|
1490
|
+
const parts = [check.state, check.context];
|
|
1491
|
+
if (check.description !== null) parts.push(check.description);
|
|
1492
|
+
let line = parts.join(" -- ");
|
|
1493
|
+
if (check.verdict === "red" && check.targetUrl !== null) line += ` (${check.targetUrl})`;
|
|
1494
|
+
return line;
|
|
1495
|
+
}
|
|
1444
1496
|
//#endregion
|
|
1445
|
-
//#region src/
|
|
1446
|
-
const DEFAULT_TIMEOUT_MS = 12e5;
|
|
1447
|
-
const DEFAULT_INTERVAL_MS = 15e3;
|
|
1448
|
-
const MIN_INTERVAL_MS = 5e3;
|
|
1497
|
+
//#region ../cli-kit/src/env-override.ts
|
|
1449
1498
|
function envMsOverride(raw, productionMs) {
|
|
1450
1499
|
if (raw === void 0) return productionMs;
|
|
1451
1500
|
const parsed = Number(raw.trim());
|
|
1452
1501
|
if (!Number.isSafeInteger(parsed) || parsed <= 0) return productionMs;
|
|
1453
1502
|
return Math.min(parsed, productionMs);
|
|
1454
1503
|
}
|
|
1504
|
+
//#endregion
|
|
1505
|
+
//#region src/poll-timings.ts
|
|
1506
|
+
const DEFAULT_TIMEOUT_MS = 12e5;
|
|
1507
|
+
const DEFAULT_INTERVAL_MS = 15e3;
|
|
1508
|
+
const MIN_INTERVAL_MS = 5e3;
|
|
1455
1509
|
function resolvePollTimings(options) {
|
|
1456
1510
|
const timeoutMs = options.timeout !== void 0 ? parseDuration(options.timeout, "--timeout") : DEFAULT_TIMEOUT_MS;
|
|
1457
1511
|
const requestedIntervalMs = options.interval !== void 0 ? parseDuration(options.interval, "--interval") : DEFAULT_INTERVAL_MS;
|
|
@@ -1528,47 +1582,23 @@ function buildReport$1(target, checks, ctx, extras = {}) {
|
|
|
1528
1582
|
return report;
|
|
1529
1583
|
}
|
|
1530
1584
|
async function waitForSettledChecks$1(fetcher, target, notices, poll) {
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
consecutiveFailures += 1;
|
|
1543
|
-
if (consecutiveFailures >= 3) return buildReport$1(target, EMPTY_CHECKS, {
|
|
1585
|
+
return settleChecks(fetcher, target.conn, poll, {
|
|
1586
|
+
sha: () => target.sha,
|
|
1587
|
+
notices,
|
|
1588
|
+
report: (checks, extras) => {
|
|
1589
|
+
const extrasForReport = {};
|
|
1590
|
+
if (extras.exit === "timeout") extrasForReport.statusOverride = "timeout";
|
|
1591
|
+
else if (extras.exit === "retry-budget") {
|
|
1592
|
+
extrasForReport.statusOverride = "unparseable";
|
|
1593
|
+
if (extras.failureDetail !== void 0) extrasForReport.detailOverride = extras.failureDetail;
|
|
1594
|
+
}
|
|
1595
|
+
return buildReport$1(target, checks, {
|
|
1544
1596
|
notices,
|
|
1545
1597
|
waited: true,
|
|
1546
|
-
elapsedMs:
|
|
1547
|
-
},
|
|
1548
|
-
statusOverride: "unparseable",
|
|
1549
|
-
detailOverride: lastFailureDetail
|
|
1550
|
-
});
|
|
1551
|
-
} else {
|
|
1552
|
-
consecutiveFailures = 0;
|
|
1553
|
-
lastGoodChecks = checks;
|
|
1554
|
-
pushDisagreementNotice(notices, sha, checks);
|
|
1555
|
-
if (isSettled(checks)) {
|
|
1556
|
-
const contexts = new Set(checks.contexts.map((check) => check.context));
|
|
1557
|
-
if (previousSettledContexts !== null && previousSettledContexts.size === contexts.size && [...previousSettledContexts].every((context) => contexts.has(context))) return buildReport$1(target, checks, {
|
|
1558
|
-
notices,
|
|
1559
|
-
waited: true,
|
|
1560
|
-
elapsedMs: now() - startedAt
|
|
1561
|
-
});
|
|
1562
|
-
previousSettledContexts = contexts;
|
|
1563
|
-
} else previousSettledContexts = null;
|
|
1598
|
+
elapsedMs: extras.elapsed()
|
|
1599
|
+
}, extrasForReport);
|
|
1564
1600
|
}
|
|
1565
|
-
|
|
1566
|
-
notices,
|
|
1567
|
-
waited: true,
|
|
1568
|
-
elapsedMs: now() - startedAt
|
|
1569
|
-
}, { statusOverride: "timeout" });
|
|
1570
|
-
await sleep(intervalMs);
|
|
1571
|
-
}
|
|
1601
|
+
});
|
|
1572
1602
|
}
|
|
1573
1603
|
async function runCommitStatus(fetcher, options) {
|
|
1574
1604
|
const timings = options.wait === true ? resolvePollTimings(options) : void 0;
|
|
@@ -1588,13 +1618,7 @@ async function runCommitStatus(fetcher, options) {
|
|
|
1588
1618
|
});
|
|
1589
1619
|
}
|
|
1590
1620
|
function renderCommitStatus(report) {
|
|
1591
|
-
const lines = report.checks.map(
|
|
1592
|
-
const parts = [check.state, check.context];
|
|
1593
|
-
if (check.description !== null) parts.push(check.description);
|
|
1594
|
-
let line = parts.join(" -- ");
|
|
1595
|
-
if (check.verdict === "red" && check.targetUrl !== null) line += ` (${check.targetUrl})`;
|
|
1596
|
-
return line;
|
|
1597
|
-
});
|
|
1621
|
+
const lines = report.checks.map(renderCheckLine);
|
|
1598
1622
|
const shortSha = FULL_SHA.test(report.sha) ? report.sha.slice(0, 12) : report.sha;
|
|
1599
1623
|
const redCount = report.checks.filter((c) => c.verdict === "red").length;
|
|
1600
1624
|
const pendingCount = report.checks.filter((c) => c.verdict === "pending").length;
|
|
@@ -1664,6 +1688,16 @@ const commitStatusCommand = defineCommand({
|
|
|
1664
1688
|
}
|
|
1665
1689
|
});
|
|
1666
1690
|
//#endregion
|
|
1691
|
+
//#region ../forgejo/src/fetcher.ts
|
|
1692
|
+
const LONG_FETCH_TIMEOUT_MS = 6e5;
|
|
1693
|
+
function morePagesFollow(res, pageLength, received, pageSize) {
|
|
1694
|
+
if (pageLength === 0) return false;
|
|
1695
|
+
const raw = res.headers.get("x-total-count")?.trim() ?? "";
|
|
1696
|
+
const total = raw === "" ? NaN : Number(raw);
|
|
1697
|
+
if (!Number.isSafeInteger(total) || total < 0) return pageLength >= pageSize;
|
|
1698
|
+
return received < total;
|
|
1699
|
+
}
|
|
1700
|
+
//#endregion
|
|
1667
1701
|
//#region ../forgejo/src/label-provisioning.ts
|
|
1668
1702
|
const LabelSchema = z.object({
|
|
1669
1703
|
id: z.number(),
|
|
@@ -1673,12 +1707,12 @@ const LabelSchema = z.object({
|
|
|
1673
1707
|
exclusive: z.boolean().optional()
|
|
1674
1708
|
});
|
|
1675
1709
|
const LabelListSchema = z.array(LabelSchema);
|
|
1676
|
-
const LABELS_PAGE_SIZE =
|
|
1710
|
+
const LABELS_PAGE_SIZE = 50;
|
|
1677
1711
|
const LABELS_MAX_PAGES = 20;
|
|
1678
1712
|
async function fetchAllForgejoLabels(executor, conn) {
|
|
1679
1713
|
const labels = [];
|
|
1680
1714
|
for (let page = 1; page <= LABELS_MAX_PAGES; page++) {
|
|
1681
|
-
const res = await executor.fetch(`${conn.serverUrl}/api/v1/repos/${conn.repository}/labels?limit=${String(LABELS_PAGE_SIZE)}&page=${String(page)}`, { headers: authHeaders
|
|
1715
|
+
const res = await executor.fetch(`${conn.serverUrl}/api/v1/repos/${conn.repository}/labels?limit=${String(LABELS_PAGE_SIZE)}&page=${String(page)}`, { headers: authHeaders(conn) });
|
|
1682
1716
|
if (!res.ok) await throwForgejoFailure(res, {
|
|
1683
1717
|
verb: "list labels",
|
|
1684
1718
|
resource: conn.repository
|
|
@@ -1686,7 +1720,7 @@ async function fetchAllForgejoLabels(executor, conn) {
|
|
|
1686
1720
|
const parsed = LabelListSchema.safeParse(await res.json());
|
|
1687
1721
|
if (!parsed.success) throw new UnexpectedError(`Unexpected label-list response: ${parsed.error.message}`);
|
|
1688
1722
|
labels.push(...parsed.data);
|
|
1689
|
-
if (parsed.data.length
|
|
1723
|
+
if (!morePagesFollow(res, parsed.data.length, labels.length, LABELS_PAGE_SIZE)) break;
|
|
1690
1724
|
}
|
|
1691
1725
|
return labels;
|
|
1692
1726
|
}
|
|
@@ -1702,6 +1736,17 @@ async function resolveLabelIds(executor, conn, names) {
|
|
|
1702
1736
|
return match.id;
|
|
1703
1737
|
});
|
|
1704
1738
|
}
|
|
1739
|
+
function isRetiringExpired(spec, today) {
|
|
1740
|
+
return today > spec.until;
|
|
1741
|
+
}
|
|
1742
|
+
function retiringAsLabelSpec(spec) {
|
|
1743
|
+
return {
|
|
1744
|
+
name: spec.name,
|
|
1745
|
+
color: spec.color,
|
|
1746
|
+
description: spec.description,
|
|
1747
|
+
exclusive: false
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1705
1750
|
const LabelSpecSchema = z.strictObject({
|
|
1706
1751
|
name: z.string().min(1),
|
|
1707
1752
|
color: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
|
@@ -1751,13 +1796,14 @@ function labelScope(name) {
|
|
|
1751
1796
|
const slash = name.indexOf("/");
|
|
1752
1797
|
return slash === -1 ? null : name.slice(0, slash + 1);
|
|
1753
1798
|
}
|
|
1754
|
-
function staleLabelNames(specs, live) {
|
|
1799
|
+
function staleLabelNames(specs, live, retiring = []) {
|
|
1755
1800
|
const owned = /* @__PURE__ */ new Set();
|
|
1756
1801
|
for (const spec of specs) {
|
|
1757
1802
|
const scope = labelScope(spec.name);
|
|
1758
1803
|
if (scope !== null) owned.add(scope);
|
|
1759
1804
|
}
|
|
1760
1805
|
const wanted = new Set(specs.map((spec) => spec.name));
|
|
1806
|
+
for (const spec of retiring) wanted.add(spec.name);
|
|
1761
1807
|
const stale = [];
|
|
1762
1808
|
for (const name of live) {
|
|
1763
1809
|
if (wanted.has(name)) continue;
|
|
@@ -1818,7 +1864,7 @@ async function ensureForgejoLabels(executor, conn, specs) {
|
|
|
1818
1864
|
async function deleteForgejoLabel(executor, conn, id) {
|
|
1819
1865
|
const res = await executor.fetch(`${conn.serverUrl}/api/v1/repos/${conn.repository}/labels/${String(id)}`, {
|
|
1820
1866
|
method: "DELETE",
|
|
1821
|
-
headers: authHeaders
|
|
1867
|
+
headers: authHeaders(conn)
|
|
1822
1868
|
}, { idempotent: true });
|
|
1823
1869
|
if (!res.ok) await throwForgejoFailure(res, {
|
|
1824
1870
|
verb: "delete label",
|
|
@@ -1871,7 +1917,7 @@ function normalizeIssue(raw) {
|
|
|
1871
1917
|
};
|
|
1872
1918
|
}
|
|
1873
1919
|
async function getForgejoIssue(executor, conn, number) {
|
|
1874
|
-
const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}`, { headers: authHeaders
|
|
1920
|
+
const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}`, { headers: authHeaders(conn) });
|
|
1875
1921
|
if (!res.ok) await throwForgejoFailure(res, {
|
|
1876
1922
|
verb: "get issue",
|
|
1877
1923
|
resource: `#${String(number)}`
|
|
@@ -1895,12 +1941,15 @@ function issueListParams(type, query) {
|
|
|
1895
1941
|
return params;
|
|
1896
1942
|
}
|
|
1897
1943
|
async function fetchIssueListPage(executor, conn, params) {
|
|
1898
|
-
const res = await executor.fetch(`${issuesBase(conn)}?${params.toString()}`, { headers: authHeaders
|
|
1944
|
+
const res = await executor.fetch(`${issuesBase(conn)}?${params.toString()}`, { headers: authHeaders(conn) });
|
|
1899
1945
|
if (!res.ok) await throwForgejoFailure(res, {
|
|
1900
1946
|
verb: "list issues",
|
|
1901
1947
|
resource: conn.repository
|
|
1902
1948
|
}, repoNotFoundMessage(conn));
|
|
1903
|
-
return
|
|
1949
|
+
return {
|
|
1950
|
+
raw: parseIssueListOrThrow(await res.json()),
|
|
1951
|
+
res
|
|
1952
|
+
};
|
|
1904
1953
|
}
|
|
1905
1954
|
function normalizeDetailedIssues(raw, type) {
|
|
1906
1955
|
const normalized = raw.map(normalizeIssue);
|
|
@@ -1911,7 +1960,7 @@ const DETAILED_LIST_MAX_PAGES = 20;
|
|
|
1911
1960
|
async function listForgejoIssuesDetailedPaged(executor, conn, query, options) {
|
|
1912
1961
|
const issues = [];
|
|
1913
1962
|
for (let page = 1; page <= DETAILED_LIST_MAX_PAGES; page++) {
|
|
1914
|
-
const raw = await fetchIssueListPage(executor, conn, issueListParams(query.type, {
|
|
1963
|
+
const { raw } = await fetchIssueListPage(executor, conn, issueListParams(query.type, {
|
|
1915
1964
|
...query,
|
|
1916
1965
|
limit: DETAILED_LIST_PAGE_SIZE,
|
|
1917
1966
|
page
|
|
@@ -1926,6 +1975,29 @@ async function listForgejoIssuesDetailedPaged(executor, conn, query, options) {
|
|
|
1926
1975
|
}
|
|
1927
1976
|
return issues;
|
|
1928
1977
|
}
|
|
1978
|
+
async function listOpenObjectsWithLabel(executor, conn, label) {
|
|
1979
|
+
const objects = [];
|
|
1980
|
+
for (let page = 1; page <= DETAILED_LIST_MAX_PAGES; page++) {
|
|
1981
|
+
const { raw } = await fetchIssueListPage(executor, conn, issueListParams("all", {
|
|
1982
|
+
state: "open",
|
|
1983
|
+
label,
|
|
1984
|
+
limit: DETAILED_LIST_PAGE_SIZE,
|
|
1985
|
+
page
|
|
1986
|
+
}));
|
|
1987
|
+
for (const issue of raw.map(normalizeIssue)) {
|
|
1988
|
+
if (!issue.labels.includes(label)) continue;
|
|
1989
|
+
objects.push({
|
|
1990
|
+
number: issue.number,
|
|
1991
|
+
kind: issue.isPullRequest ? "pull" : "issue",
|
|
1992
|
+
title: issue.title,
|
|
1993
|
+
url: issue.url
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
if (raw.length === 0) break;
|
|
1997
|
+
if (page === DETAILED_LIST_MAX_PAGES) log.warn(`Open-object listing on ${conn.repository} hit the ${String(DETAILED_LIST_MAX_PAGES)}-page cap without an empty page; results may be truncated`);
|
|
1998
|
+
}
|
|
1999
|
+
return objects;
|
|
2000
|
+
}
|
|
1929
2001
|
const USER_REPOS_PAGE_SIZE = 50;
|
|
1930
2002
|
const USER_REPOS_MAX_PAGES = 20;
|
|
1931
2003
|
const UserRepoSchema = z.object({
|
|
@@ -2046,24 +2118,77 @@ function outcomeFromAudit(conn, specs, audit, lists) {
|
|
|
2046
2118
|
...lists
|
|
2047
2119
|
};
|
|
2048
2120
|
}
|
|
2121
|
+
async function checkPruneCandidate(fetcher, conn, name) {
|
|
2122
|
+
let objects;
|
|
2123
|
+
try {
|
|
2124
|
+
objects = await listOpenObjectsWithLabel(fetcher, conn, name);
|
|
2125
|
+
} catch (error) {
|
|
2126
|
+
return {
|
|
2127
|
+
ok: false,
|
|
2128
|
+
reason: `could not check open objects: ${describeError(error)}`,
|
|
2129
|
+
objects: []
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
if (objects.length === 0) return { ok: true };
|
|
2133
|
+
return {
|
|
2134
|
+
ok: false,
|
|
2135
|
+
reason: `on ${String(objects.length)} open object(s)`,
|
|
2136
|
+
objects
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2049
2139
|
async function syncOneRepo(params) {
|
|
2050
|
-
const { fetcher, conn, specs, mode, prune, allowedPruneNames, resolveTargetSpecs } = params;
|
|
2140
|
+
const { fetcher, conn, specs, mode, prune, allowedPruneNames, resolveTargetSpecs, retiring, today } = params;
|
|
2051
2141
|
const target = resolveTargetSpecs === void 0 ? {
|
|
2052
2142
|
specs,
|
|
2053
2143
|
omitted: [],
|
|
2054
2144
|
aliasNames: []
|
|
2055
2145
|
} : await resolveTargetSpecs(conn, specs);
|
|
2056
|
-
const
|
|
2057
|
-
const
|
|
2058
|
-
const
|
|
2146
|
+
const retiringNames = new Set(retiring.map((spec) => spec.name));
|
|
2147
|
+
const fullAudit = await auditForgejoLabels(fetcher, conn, [...target.specs, ...retiring.map(retiringAsLabelSpec)]);
|
|
2148
|
+
const presentRetiring = retiring.filter((spec) => fullAudit.labelIds.has(spec.name));
|
|
2149
|
+
const audit = {
|
|
2150
|
+
...fullAudit,
|
|
2151
|
+
missing: fullAudit.missing.filter((name) => !retiringNames.has(name))
|
|
2152
|
+
};
|
|
2153
|
+
const stale = staleLabelNames(specs, audit.labelIds.keys(), retiring).filter((name) => !target.aliasNames.includes(name));
|
|
2154
|
+
const expiredRetiring = presentRetiring.filter((spec) => isRetiringExpired(spec, today)).map((spec) => spec.name);
|
|
2155
|
+
const pruneCandidates = prune ? [
|
|
2156
|
+
...target.omitted,
|
|
2157
|
+
...stale,
|
|
2158
|
+
...expiredRetiring
|
|
2159
|
+
].filter((name) => audit.labelIds.has(name) && (allowedPruneNames === void 0 || allowedPruneNames.includes(name))) : [];
|
|
2160
|
+
const pruned = [];
|
|
2161
|
+
const refused = [];
|
|
2162
|
+
for (const name of pruneCandidates) {
|
|
2163
|
+
const verdict = await checkPruneCandidate(fetcher, conn, name);
|
|
2164
|
+
if (verdict.ok) {
|
|
2165
|
+
pruned.push(name);
|
|
2166
|
+
continue;
|
|
2167
|
+
}
|
|
2168
|
+
refused.push({
|
|
2169
|
+
name,
|
|
2170
|
+
reason: verdict.reason,
|
|
2171
|
+
objects: verdict.objects
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
const retiringReports = [];
|
|
2175
|
+
for (const spec of presentRetiring) retiringReports.push({
|
|
2176
|
+
name: spec.name,
|
|
2177
|
+
replacement: spec.replacement,
|
|
2178
|
+
until: spec.until,
|
|
2179
|
+
expired: isRetiringExpired(spec, today),
|
|
2180
|
+
openObjects: await listOpenObjectsWithLabel(fetcher, conn, spec.name)
|
|
2181
|
+
});
|
|
2059
2182
|
const outcome = outcomeFromAudit(conn, target.specs, audit, {
|
|
2060
2183
|
omitted: target.omitted,
|
|
2061
|
-
pruned
|
|
2062
|
-
stale: prune ? stale.filter((name) => !pruneCandidates.includes(name)) : stale
|
|
2184
|
+
pruned,
|
|
2185
|
+
stale: prune ? stale.filter((name) => !pruneCandidates.includes(name)) : stale,
|
|
2186
|
+
retiring: retiringReports,
|
|
2187
|
+
refused
|
|
2063
2188
|
});
|
|
2064
2189
|
if (mode === "apply") {
|
|
2065
|
-
if (outcome.created.length > 0 || outcome.updated.length > 0) await ensureForgejoLabels(fetcher, conn, target.specs);
|
|
2066
|
-
for (const name of
|
|
2190
|
+
if (outcome.created.length > 0 || outcome.updated.length > 0) await ensureForgejoLabels(fetcher, conn, [...target.specs, ...presentRetiring.map(retiringAsLabelSpec)]);
|
|
2191
|
+
for (const name of pruned) {
|
|
2067
2192
|
const id = audit.labelIds.get(name);
|
|
2068
2193
|
if (id === void 0) throw new UnexpectedError(`Prune candidate "${name}" had no known id after the audit`);
|
|
2069
2194
|
await deleteForgejoLabel(fetcher, conn, id);
|
|
@@ -2073,6 +2198,9 @@ async function syncOneRepo(params) {
|
|
|
2073
2198
|
}
|
|
2074
2199
|
async function syncLabelSet(params) {
|
|
2075
2200
|
const { fetcher, targets, specs, setName, mode, contained, prune, pruneAllowlist } = params;
|
|
2201
|
+
const retiring = params.retiring ?? [];
|
|
2202
|
+
const today = params.today ?? "";
|
|
2203
|
+
if (retiring.length > 0 && params.today === void 0) throw new UnexpectedError("syncLabelSet was given retiring label specs but no `today` to compare their expiry against: an expired entry is a prune candidate, and a guessed date could delete a name whose retirement window is still open.");
|
|
2076
2204
|
const specList = [...specs];
|
|
2077
2205
|
const repos = [];
|
|
2078
2206
|
const failures = [];
|
|
@@ -2085,7 +2213,9 @@ async function syncLabelSet(params) {
|
|
|
2085
2213
|
mode,
|
|
2086
2214
|
prune: prune === true,
|
|
2087
2215
|
allowedPruneNames,
|
|
2088
|
-
resolveTargetSpecs: params.resolveTargetSpecs
|
|
2216
|
+
resolveTargetSpecs: params.resolveTargetSpecs,
|
|
2217
|
+
retiring,
|
|
2218
|
+
today
|
|
2089
2219
|
};
|
|
2090
2220
|
if (contained !== true) {
|
|
2091
2221
|
repos.push(await syncOneRepo(syncOneRepoParams));
|
|
@@ -2118,6 +2248,11 @@ function syncSummaryLines(result) {
|
|
|
2118
2248
|
if (repo.omitted.length > 0) actions.push(`omitted (repo declares its own priority vocabulary): ${repo.omitted.join(", ")}`);
|
|
2119
2249
|
if (repo.pruned.length > 0) actions.push(`${verb}prune: ${repo.pruned.join(", ")}`);
|
|
2120
2250
|
if (repo.stale.length > 0) actions.push(`stale (present, not in this set): ${repo.stale.join(", ")}`);
|
|
2251
|
+
for (const refusal of repo.refused) {
|
|
2252
|
+
const objects = refusal.objects.map((object) => `#${String(object.number)} (${object.kind}) ${object.title}`).join(", ");
|
|
2253
|
+
actions.push(`refused to prune ${refusal.name}: ${refusal.reason}${objects === "" ? "" : ` (${objects})`}`);
|
|
2254
|
+
}
|
|
2255
|
+
for (const retiring of repo.retiring) actions.push(`retiring ${retiring.name}: replaced by ${retiring.replacement}, prunable after ${retiring.until}${retiring.expired ? " (expired)" : ""}; ${String(retiring.openObjects.length)} open object(s)`);
|
|
2121
2256
|
if (actions.length === 0) {
|
|
2122
2257
|
repoLines.push(`${repo.repository}: up to date (${String(repo.unchanged.length)} unchanged)`);
|
|
2123
2258
|
continue;
|
|
@@ -2257,7 +2392,7 @@ const syncCommand = defineCommand({
|
|
|
2257
2392
|
},
|
|
2258
2393
|
json: {
|
|
2259
2394
|
type: "boolean",
|
|
2260
|
-
description: "Emit one JSON document: set, mode (apply | check | dry-run), repos ([{repository, created, updated, unchanged, omitted, stale}], every field but repository a list of label names), failures ([{repository, error}]; always present, filled only under --fleet), drifted. drifted is true when any label is missing or drifted, and sets the exit code only under --check."
|
|
2395
|
+
description: "Emit one JSON document: set, mode (apply | check | dry-run), repos ([{repository, created, updated, unchanged, omitted, stale, retiring, refused}], every field but repository and those two a list of label names; retiring and refused are always empty here, since this command never prunes), failures ([{repository, error}]; always present, filled only under --fleet), drifted. drifted is true when any label is missing or drifted, and sets the exit code only under --check."
|
|
2261
2396
|
}
|
|
2262
2397
|
},
|
|
2263
2398
|
async run({ args, rawArgs }) {
|
|
@@ -2301,9 +2436,6 @@ const labelsCommand = defineCommand({
|
|
|
2301
2436
|
}
|
|
2302
2437
|
});
|
|
2303
2438
|
//#endregion
|
|
2304
|
-
//#region ../forgejo/src/fetcher.ts
|
|
2305
|
-
const LONG_FETCH_TIMEOUT_MS = 6e5;
|
|
2306
|
-
//#endregion
|
|
2307
2439
|
//#region ../forgejo/src/releases.ts
|
|
2308
2440
|
const SETTLE_ATTEMPTS = 4;
|
|
2309
2441
|
const SETTLE_BASE_DELAY_MS = 2e3;
|
|
@@ -2814,7 +2946,7 @@ const AuthenticatedUserSchema = z.object({
|
|
|
2814
2946
|
});
|
|
2815
2947
|
async function getAuthenticatedUser(executor, conn, signal = null) {
|
|
2816
2948
|
const res = await executor.fetch(`${conn.serverUrl}/api/v1/user`, {
|
|
2817
|
-
headers: authHeaders
|
|
2949
|
+
headers: authHeaders(conn),
|
|
2818
2950
|
signal
|
|
2819
2951
|
});
|
|
2820
2952
|
if (!res.ok) return {
|
|
@@ -2891,7 +3023,7 @@ async function paginate(executor, conn, endpoint, verb, extract) {
|
|
|
2891
3023
|
}));
|
|
2892
3024
|
const items = extract(await res.json());
|
|
2893
3025
|
all.push(...items);
|
|
2894
|
-
if (items.length
|
|
3026
|
+
if (!morePagesFollow(res, items.length, all.length, PAGE_LIMIT)) break;
|
|
2895
3027
|
if (page === 101) throw new UnexpectedError(`${endpoint} still returns full pages after ${String(MAX_PAGES)} pages; refusing to report a prefix of it`);
|
|
2896
3028
|
}
|
|
2897
3029
|
return all;
|
|
@@ -3360,11 +3492,6 @@ const ciLogCommand = defineCommand({
|
|
|
3360
3492
|
});
|
|
3361
3493
|
//#endregion
|
|
3362
3494
|
//#region src/ci-runs.ts
|
|
3363
|
-
function parseLimit$2(raw) {
|
|
3364
|
-
const limit = Number(raw);
|
|
3365
|
-
if (!Number.isInteger(limit) || limit < 1) throw new FatalError(`Invalid --limit "${raw}" (expected a positive integer).`);
|
|
3366
|
-
return limit;
|
|
3367
|
-
}
|
|
3368
3495
|
function resolveRunsRef(ref, pr) {
|
|
3369
3496
|
if (ref !== void 0 && pr !== void 0) throw new FatalError("--ref and --pr are the same filter -- give one of them (--pr 273 for a pull request's own runs, --ref main for a branch's).");
|
|
3370
3497
|
if (pr !== void 0) {
|
|
@@ -3389,7 +3516,7 @@ function resolveRunsRef(ref, pr) {
|
|
|
3389
3516
|
};
|
|
3390
3517
|
}
|
|
3391
3518
|
async function runCiRuns(fetcher, options) {
|
|
3392
|
-
const limit =
|
|
3519
|
+
const limit = parseLimitFlag(options.limit);
|
|
3393
3520
|
const resolved = resolveRunsRef(options.ref, options.pr);
|
|
3394
3521
|
const conn = requireForgejoTarget(options.cwd, resolveIssueTarget(resolved.target, options.repo), "ci:runs is Forgejo-only -- gh run list already serves GitHub, first-class. Use that instead.");
|
|
3395
3522
|
rootLogger.debug(`ci:runs repository=${conn.repository} limit=${String(limit)}`);
|
|
@@ -3634,12 +3761,6 @@ const BranchEntrySchema = z.object({
|
|
|
3634
3761
|
});
|
|
3635
3762
|
const BRANCHES_PAGE_SIZE = 50;
|
|
3636
3763
|
const DEFAULT_MAX_PAGES = 20;
|
|
3637
|
-
function repoBase(conn) {
|
|
3638
|
-
return `${conn.serverUrl}/api/v1/repos/${conn.repository}`;
|
|
3639
|
-
}
|
|
3640
|
-
function authHeaders(conn) {
|
|
3641
|
-
return { Authorization: `token ${conn.token}` };
|
|
3642
|
-
}
|
|
3643
3764
|
async function listBranches(executor, conn, options = {}) {
|
|
3644
3765
|
const signal = options.signal;
|
|
3645
3766
|
const maxPages = options.maxPages ?? DEFAULT_MAX_PAGES;
|
|
@@ -3869,14 +3990,14 @@ async function listForgejoComments(executor, conn, number, options = {}) {
|
|
|
3869
3990
|
limit: String(COMMENTS_PAGE_SIZE),
|
|
3870
3991
|
page: String(page)
|
|
3871
3992
|
});
|
|
3872
|
-
const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}/comments?${params.toString()}`, { headers: authHeaders
|
|
3993
|
+
const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}/comments?${params.toString()}`, { headers: authHeaders(conn) });
|
|
3873
3994
|
if (!res.ok) await throwForgejoFailure(res, {
|
|
3874
3995
|
verb: "list comments",
|
|
3875
3996
|
resource: `#${String(number)}`
|
|
3876
3997
|
}, issueNotFoundMessage(conn, number));
|
|
3877
3998
|
const parsed = parseCommentListOrThrow(await res.json());
|
|
3878
3999
|
comments.push(...parsed.map(normalizeComment));
|
|
3879
|
-
if (parsed.length
|
|
4000
|
+
if (!morePagesFollow(res, parsed.length, comments.length, COMMENTS_PAGE_SIZE)) break;
|
|
3880
4001
|
if (page === COMMENTS_MAX_PAGES) {
|
|
3881
4002
|
log.warn(`Comment listing for #${String(number)} hit the ${String(COMMENTS_MAX_PAGES)}-page cap; results may be truncated`);
|
|
3882
4003
|
options.onPageCap?.();
|
|
@@ -4352,7 +4473,7 @@ function parseState$1(state) {
|
|
|
4352
4473
|
if (state === "open" || state === "closed" || state === "all") return state;
|
|
4353
4474
|
throw new InvalidListQueryError("state", state);
|
|
4354
4475
|
}
|
|
4355
|
-
function parseLimit
|
|
4476
|
+
function parseLimit(raw) {
|
|
4356
4477
|
const limit = Number(raw);
|
|
4357
4478
|
if (!Number.isInteger(limit) || limit <= 0) throw new InvalidListQueryError("limit", raw);
|
|
4358
4479
|
return limit;
|
|
@@ -4365,7 +4486,7 @@ async function listIssues(resolve, request) {
|
|
|
4365
4486
|
const label = normalizeLabel(request.label);
|
|
4366
4487
|
const query = {
|
|
4367
4488
|
state: parseState$1(request.state),
|
|
4368
|
-
limit: parseLimit
|
|
4489
|
+
limit: parseLimit(request.limit),
|
|
4369
4490
|
...label === void 0 ? {} : { label }
|
|
4370
4491
|
};
|
|
4371
4492
|
const { api } = resolve(parseRepoTarget(request.repo));
|
|
@@ -4847,7 +4968,7 @@ async function removeForgejoIssueLabel(executor, conn, number, name, labelIds) {
|
|
|
4847
4968
|
const id = await resolveLabelId(executor, conn, name, labelIds);
|
|
4848
4969
|
const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}/labels/${String(id)}`, {
|
|
4849
4970
|
method: "DELETE",
|
|
4850
|
-
headers: authHeaders
|
|
4971
|
+
headers: authHeaders(conn)
|
|
4851
4972
|
}, { idempotent: true });
|
|
4852
4973
|
if (!res.ok) await throwForgejoFailure(res, {
|
|
4853
4974
|
verb: "remove label",
|
|
@@ -5511,75 +5632,45 @@ function buildReport(detail, checks, ctx, extras = {}) {
|
|
|
5511
5632
|
return report;
|
|
5512
5633
|
}
|
|
5513
5634
|
async function waitForSettledChecks(fetcher, target, baseline, notices, poll) {
|
|
5514
|
-
const { timeoutMs, intervalMs, sleep, now } = poll;
|
|
5515
|
-
const startedAt = now();
|
|
5516
5635
|
let currentDetail = target.detail;
|
|
5517
|
-
let consecutiveFailures = 0;
|
|
5518
5636
|
let consecutiveDetailFailures = 0;
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
if (now() - startedAt >= timeoutMs) return buildReport(currentDetail, lastGoodChecks ?? EMPTY_CHECKS, {
|
|
5555
|
-
target,
|
|
5556
|
-
baseline,
|
|
5557
|
-
notices,
|
|
5558
|
-
waited: true,
|
|
5559
|
-
elapsedMs: now() - startedAt
|
|
5560
|
-
}, { statusOverride: "timeout" });
|
|
5561
|
-
await sleep(intervalMs);
|
|
5562
|
-
let nextDetail;
|
|
5563
|
-
try {
|
|
5564
|
-
nextDetail = await getForgejoPrDetail(fetcher, target.conn, target.prNumber);
|
|
5565
|
-
consecutiveDetailFailures = 0;
|
|
5566
|
-
} catch (error) {
|
|
5567
|
-
consecutiveDetailFailures += 1;
|
|
5568
|
-
if (consecutiveDetailFailures >= 3) throw error;
|
|
5569
|
-
continue;
|
|
5637
|
+
const reportFor = (detail, checks, elapsedMs, extras = {}) => buildReport(detail, checks, {
|
|
5638
|
+
target,
|
|
5639
|
+
baseline,
|
|
5640
|
+
notices,
|
|
5641
|
+
waited: true,
|
|
5642
|
+
elapsedMs
|
|
5643
|
+
}, extras);
|
|
5644
|
+
return settleChecks(fetcher, target.conn, poll, {
|
|
5645
|
+
sha: () => currentDetail.head.sha,
|
|
5646
|
+
notices,
|
|
5647
|
+
report: (checks, extras) => {
|
|
5648
|
+
const reportExtras = {};
|
|
5649
|
+
if (extras.exit === "timeout") reportExtras.statusOverride = "timeout";
|
|
5650
|
+
else if (extras.exit === "retry-budget") {
|
|
5651
|
+
reportExtras.statusOverride = "unparseable";
|
|
5652
|
+
if (extras.failureDetail !== void 0) reportExtras.detailOverride = extras.failureDetail;
|
|
5653
|
+
}
|
|
5654
|
+
return reportFor(currentDetail, checks, extras.elapsed(), reportExtras);
|
|
5655
|
+
},
|
|
5656
|
+
afterSleep: async (sha, elapsed) => {
|
|
5657
|
+
let nextDetail;
|
|
5658
|
+
try {
|
|
5659
|
+
nextDetail = await getForgejoPrDetail(fetcher, target.conn, target.prNumber);
|
|
5660
|
+
consecutiveDetailFailures = 0;
|
|
5661
|
+
} catch (error) {
|
|
5662
|
+
consecutiveDetailFailures += 1;
|
|
5663
|
+
if (consecutiveDetailFailures >= 3) throw error;
|
|
5664
|
+
return "retry";
|
|
5665
|
+
}
|
|
5666
|
+
if (nextDetail.head.sha !== sha) return { done: reportFor(nextDetail, EMPTY_CHECKS, elapsed(), {
|
|
5667
|
+
statusOverride: "head-moved",
|
|
5668
|
+
previousHeadSha: sha
|
|
5669
|
+
}) };
|
|
5670
|
+
currentDetail = nextDetail;
|
|
5671
|
+
return "next";
|
|
5570
5672
|
}
|
|
5571
|
-
|
|
5572
|
-
target,
|
|
5573
|
-
baseline,
|
|
5574
|
-
notices,
|
|
5575
|
-
waited: true,
|
|
5576
|
-
elapsedMs: now() - startedAt
|
|
5577
|
-
}, {
|
|
5578
|
-
statusOverride: "head-moved",
|
|
5579
|
-
previousHeadSha: sha
|
|
5580
|
-
});
|
|
5581
|
-
currentDetail = nextDetail;
|
|
5582
|
-
}
|
|
5673
|
+
});
|
|
5583
5674
|
}
|
|
5584
5675
|
async function runPrChecks(fetcher, options) {
|
|
5585
5676
|
const timings = options.wait === true ? resolvePollTimings(options) : void 0;
|
|
@@ -5612,13 +5703,7 @@ function baselineClause(report) {
|
|
|
5612
5703
|
return `${String(redChecks.length)} red, ${String(preexistingCount)} of them already red on ` + report.baseline.ref;
|
|
5613
5704
|
}
|
|
5614
5705
|
function renderPrChecks(report) {
|
|
5615
|
-
const lines = report.checks.map((check) =>
|
|
5616
|
-
const parts = [check.state, check.context];
|
|
5617
|
-
if (check.description !== null) parts.push(check.description);
|
|
5618
|
-
let line = parts.join(" -- ");
|
|
5619
|
-
if (check.verdict === "red" && check.targetUrl !== null) line += ` (${check.targetUrl})`;
|
|
5620
|
-
return line;
|
|
5621
|
-
});
|
|
5706
|
+
const lines = report.checks.map((check) => renderCheckLine(check));
|
|
5622
5707
|
const summaryParts = [`head ${report.headSha.slice(0, 12)}: ${report.status}`];
|
|
5623
5708
|
const clause = baselineClause(report);
|
|
5624
5709
|
if (clause !== null) summaryParts.push(clause);
|
|
@@ -6533,14 +6618,9 @@ function parseState(state) {
|
|
|
6533
6618
|
if (state === "open" || state === "closed" || state === "all") return state;
|
|
6534
6619
|
throw new FatalError(`Invalid --state "${state}" (expected open, closed, or all).`);
|
|
6535
6620
|
}
|
|
6536
|
-
function parseLimit(raw) {
|
|
6537
|
-
const limit = Number(raw);
|
|
6538
|
-
if (!Number.isInteger(limit) || limit < 1) throw new FatalError(`Invalid --limit "${raw}" (expected a positive integer).`);
|
|
6539
|
-
return limit;
|
|
6540
|
-
}
|
|
6541
6621
|
async function runPrList(fetcher, options) {
|
|
6542
6622
|
const state = parseState(options.state);
|
|
6543
|
-
const limit =
|
|
6623
|
+
const limit = parseLimitFlag(options.limit);
|
|
6544
6624
|
rootLogger.debug(`pr:list state=${state} limit=${String(limit)}`);
|
|
6545
6625
|
const conn = requireForgejoTarget(options.cwd, parseRepoTarget(options.repo), "pr:list is Forgejo-only -- gh pr list already serves GitHub, first-class. Use that instead.");
|
|
6546
6626
|
const notices = [];
|