@bensandee/forgejo-cli 0.5.6 → 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 +244 -174
- 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;
|
|
@@ -1688,7 +1712,7 @@ const LABELS_MAX_PAGES = 20;
|
|
|
1688
1712
|
async function fetchAllForgejoLabels(executor, conn) {
|
|
1689
1713
|
const labels = [];
|
|
1690
1714
|
for (let page = 1; page <= LABELS_MAX_PAGES; page++) {
|
|
1691
|
-
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) });
|
|
1692
1716
|
if (!res.ok) await throwForgejoFailure(res, {
|
|
1693
1717
|
verb: "list labels",
|
|
1694
1718
|
resource: conn.repository
|
|
@@ -1712,6 +1736,17 @@ async function resolveLabelIds(executor, conn, names) {
|
|
|
1712
1736
|
return match.id;
|
|
1713
1737
|
});
|
|
1714
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
|
+
}
|
|
1715
1750
|
const LabelSpecSchema = z.strictObject({
|
|
1716
1751
|
name: z.string().min(1),
|
|
1717
1752
|
color: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
|
@@ -1761,13 +1796,14 @@ function labelScope(name) {
|
|
|
1761
1796
|
const slash = name.indexOf("/");
|
|
1762
1797
|
return slash === -1 ? null : name.slice(0, slash + 1);
|
|
1763
1798
|
}
|
|
1764
|
-
function staleLabelNames(specs, live) {
|
|
1799
|
+
function staleLabelNames(specs, live, retiring = []) {
|
|
1765
1800
|
const owned = /* @__PURE__ */ new Set();
|
|
1766
1801
|
for (const spec of specs) {
|
|
1767
1802
|
const scope = labelScope(spec.name);
|
|
1768
1803
|
if (scope !== null) owned.add(scope);
|
|
1769
1804
|
}
|
|
1770
1805
|
const wanted = new Set(specs.map((spec) => spec.name));
|
|
1806
|
+
for (const spec of retiring) wanted.add(spec.name);
|
|
1771
1807
|
const stale = [];
|
|
1772
1808
|
for (const name of live) {
|
|
1773
1809
|
if (wanted.has(name)) continue;
|
|
@@ -1828,7 +1864,7 @@ async function ensureForgejoLabels(executor, conn, specs) {
|
|
|
1828
1864
|
async function deleteForgejoLabel(executor, conn, id) {
|
|
1829
1865
|
const res = await executor.fetch(`${conn.serverUrl}/api/v1/repos/${conn.repository}/labels/${String(id)}`, {
|
|
1830
1866
|
method: "DELETE",
|
|
1831
|
-
headers: authHeaders
|
|
1867
|
+
headers: authHeaders(conn)
|
|
1832
1868
|
}, { idempotent: true });
|
|
1833
1869
|
if (!res.ok) await throwForgejoFailure(res, {
|
|
1834
1870
|
verb: "delete label",
|
|
@@ -1881,7 +1917,7 @@ function normalizeIssue(raw) {
|
|
|
1881
1917
|
};
|
|
1882
1918
|
}
|
|
1883
1919
|
async function getForgejoIssue(executor, conn, number) {
|
|
1884
|
-
const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}`, { headers: authHeaders
|
|
1920
|
+
const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}`, { headers: authHeaders(conn) });
|
|
1885
1921
|
if (!res.ok) await throwForgejoFailure(res, {
|
|
1886
1922
|
verb: "get issue",
|
|
1887
1923
|
resource: `#${String(number)}`
|
|
@@ -1905,7 +1941,7 @@ function issueListParams(type, query) {
|
|
|
1905
1941
|
return params;
|
|
1906
1942
|
}
|
|
1907
1943
|
async function fetchIssueListPage(executor, conn, params) {
|
|
1908
|
-
const res = await executor.fetch(`${issuesBase(conn)}?${params.toString()}`, { headers: authHeaders
|
|
1944
|
+
const res = await executor.fetch(`${issuesBase(conn)}?${params.toString()}`, { headers: authHeaders(conn) });
|
|
1909
1945
|
if (!res.ok) await throwForgejoFailure(res, {
|
|
1910
1946
|
verb: "list issues",
|
|
1911
1947
|
resource: conn.repository
|
|
@@ -1939,6 +1975,29 @@ async function listForgejoIssuesDetailedPaged(executor, conn, query, options) {
|
|
|
1939
1975
|
}
|
|
1940
1976
|
return issues;
|
|
1941
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
|
+
}
|
|
1942
2001
|
const USER_REPOS_PAGE_SIZE = 50;
|
|
1943
2002
|
const USER_REPOS_MAX_PAGES = 20;
|
|
1944
2003
|
const UserRepoSchema = z.object({
|
|
@@ -2059,24 +2118,77 @@ function outcomeFromAudit(conn, specs, audit, lists) {
|
|
|
2059
2118
|
...lists
|
|
2060
2119
|
};
|
|
2061
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
|
+
}
|
|
2062
2139
|
async function syncOneRepo(params) {
|
|
2063
|
-
const { fetcher, conn, specs, mode, prune, allowedPruneNames, resolveTargetSpecs } = params;
|
|
2140
|
+
const { fetcher, conn, specs, mode, prune, allowedPruneNames, resolveTargetSpecs, retiring, today } = params;
|
|
2064
2141
|
const target = resolveTargetSpecs === void 0 ? {
|
|
2065
2142
|
specs,
|
|
2066
2143
|
omitted: [],
|
|
2067
2144
|
aliasNames: []
|
|
2068
2145
|
} : await resolveTargetSpecs(conn, specs);
|
|
2069
|
-
const
|
|
2070
|
-
const
|
|
2071
|
-
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
|
+
});
|
|
2072
2182
|
const outcome = outcomeFromAudit(conn, target.specs, audit, {
|
|
2073
2183
|
omitted: target.omitted,
|
|
2074
|
-
pruned
|
|
2075
|
-
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
|
|
2076
2188
|
});
|
|
2077
2189
|
if (mode === "apply") {
|
|
2078
|
-
if (outcome.created.length > 0 || outcome.updated.length > 0) await ensureForgejoLabels(fetcher, conn, target.specs);
|
|
2079
|
-
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) {
|
|
2080
2192
|
const id = audit.labelIds.get(name);
|
|
2081
2193
|
if (id === void 0) throw new UnexpectedError(`Prune candidate "${name}" had no known id after the audit`);
|
|
2082
2194
|
await deleteForgejoLabel(fetcher, conn, id);
|
|
@@ -2086,6 +2198,9 @@ async function syncOneRepo(params) {
|
|
|
2086
2198
|
}
|
|
2087
2199
|
async function syncLabelSet(params) {
|
|
2088
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.");
|
|
2089
2204
|
const specList = [...specs];
|
|
2090
2205
|
const repos = [];
|
|
2091
2206
|
const failures = [];
|
|
@@ -2098,7 +2213,9 @@ async function syncLabelSet(params) {
|
|
|
2098
2213
|
mode,
|
|
2099
2214
|
prune: prune === true,
|
|
2100
2215
|
allowedPruneNames,
|
|
2101
|
-
resolveTargetSpecs: params.resolveTargetSpecs
|
|
2216
|
+
resolveTargetSpecs: params.resolveTargetSpecs,
|
|
2217
|
+
retiring,
|
|
2218
|
+
today
|
|
2102
2219
|
};
|
|
2103
2220
|
if (contained !== true) {
|
|
2104
2221
|
repos.push(await syncOneRepo(syncOneRepoParams));
|
|
@@ -2131,6 +2248,11 @@ function syncSummaryLines(result) {
|
|
|
2131
2248
|
if (repo.omitted.length > 0) actions.push(`omitted (repo declares its own priority vocabulary): ${repo.omitted.join(", ")}`);
|
|
2132
2249
|
if (repo.pruned.length > 0) actions.push(`${verb}prune: ${repo.pruned.join(", ")}`);
|
|
2133
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)`);
|
|
2134
2256
|
if (actions.length === 0) {
|
|
2135
2257
|
repoLines.push(`${repo.repository}: up to date (${String(repo.unchanged.length)} unchanged)`);
|
|
2136
2258
|
continue;
|
|
@@ -2270,7 +2392,7 @@ const syncCommand = defineCommand({
|
|
|
2270
2392
|
},
|
|
2271
2393
|
json: {
|
|
2272
2394
|
type: "boolean",
|
|
2273
|
-
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."
|
|
2274
2396
|
}
|
|
2275
2397
|
},
|
|
2276
2398
|
async run({ args, rawArgs }) {
|
|
@@ -2824,7 +2946,7 @@ const AuthenticatedUserSchema = z.object({
|
|
|
2824
2946
|
});
|
|
2825
2947
|
async function getAuthenticatedUser(executor, conn, signal = null) {
|
|
2826
2948
|
const res = await executor.fetch(`${conn.serverUrl}/api/v1/user`, {
|
|
2827
|
-
headers: authHeaders
|
|
2949
|
+
headers: authHeaders(conn),
|
|
2828
2950
|
signal
|
|
2829
2951
|
});
|
|
2830
2952
|
if (!res.ok) return {
|
|
@@ -3370,11 +3492,6 @@ const ciLogCommand = defineCommand({
|
|
|
3370
3492
|
});
|
|
3371
3493
|
//#endregion
|
|
3372
3494
|
//#region src/ci-runs.ts
|
|
3373
|
-
function parseLimit$2(raw) {
|
|
3374
|
-
const limit = Number(raw);
|
|
3375
|
-
if (!Number.isInteger(limit) || limit < 1) throw new FatalError(`Invalid --limit "${raw}" (expected a positive integer).`);
|
|
3376
|
-
return limit;
|
|
3377
|
-
}
|
|
3378
3495
|
function resolveRunsRef(ref, pr) {
|
|
3379
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).");
|
|
3380
3497
|
if (pr !== void 0) {
|
|
@@ -3399,7 +3516,7 @@ function resolveRunsRef(ref, pr) {
|
|
|
3399
3516
|
};
|
|
3400
3517
|
}
|
|
3401
3518
|
async function runCiRuns(fetcher, options) {
|
|
3402
|
-
const limit =
|
|
3519
|
+
const limit = parseLimitFlag(options.limit);
|
|
3403
3520
|
const resolved = resolveRunsRef(options.ref, options.pr);
|
|
3404
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.");
|
|
3405
3522
|
rootLogger.debug(`ci:runs repository=${conn.repository} limit=${String(limit)}`);
|
|
@@ -3644,12 +3761,6 @@ const BranchEntrySchema = z.object({
|
|
|
3644
3761
|
});
|
|
3645
3762
|
const BRANCHES_PAGE_SIZE = 50;
|
|
3646
3763
|
const DEFAULT_MAX_PAGES = 20;
|
|
3647
|
-
function repoBase(conn) {
|
|
3648
|
-
return `${conn.serverUrl}/api/v1/repos/${conn.repository}`;
|
|
3649
|
-
}
|
|
3650
|
-
function authHeaders(conn) {
|
|
3651
|
-
return { Authorization: `token ${conn.token}` };
|
|
3652
|
-
}
|
|
3653
3764
|
async function listBranches(executor, conn, options = {}) {
|
|
3654
3765
|
const signal = options.signal;
|
|
3655
3766
|
const maxPages = options.maxPages ?? DEFAULT_MAX_PAGES;
|
|
@@ -3879,7 +3990,7 @@ async function listForgejoComments(executor, conn, number, options = {}) {
|
|
|
3879
3990
|
limit: String(COMMENTS_PAGE_SIZE),
|
|
3880
3991
|
page: String(page)
|
|
3881
3992
|
});
|
|
3882
|
-
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) });
|
|
3883
3994
|
if (!res.ok) await throwForgejoFailure(res, {
|
|
3884
3995
|
verb: "list comments",
|
|
3885
3996
|
resource: `#${String(number)}`
|
|
@@ -4362,7 +4473,7 @@ function parseState$1(state) {
|
|
|
4362
4473
|
if (state === "open" || state === "closed" || state === "all") return state;
|
|
4363
4474
|
throw new InvalidListQueryError("state", state);
|
|
4364
4475
|
}
|
|
4365
|
-
function parseLimit
|
|
4476
|
+
function parseLimit(raw) {
|
|
4366
4477
|
const limit = Number(raw);
|
|
4367
4478
|
if (!Number.isInteger(limit) || limit <= 0) throw new InvalidListQueryError("limit", raw);
|
|
4368
4479
|
return limit;
|
|
@@ -4375,7 +4486,7 @@ async function listIssues(resolve, request) {
|
|
|
4375
4486
|
const label = normalizeLabel(request.label);
|
|
4376
4487
|
const query = {
|
|
4377
4488
|
state: parseState$1(request.state),
|
|
4378
|
-
limit: parseLimit
|
|
4489
|
+
limit: parseLimit(request.limit),
|
|
4379
4490
|
...label === void 0 ? {} : { label }
|
|
4380
4491
|
};
|
|
4381
4492
|
const { api } = resolve(parseRepoTarget(request.repo));
|
|
@@ -4857,7 +4968,7 @@ async function removeForgejoIssueLabel(executor, conn, number, name, labelIds) {
|
|
|
4857
4968
|
const id = await resolveLabelId(executor, conn, name, labelIds);
|
|
4858
4969
|
const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}/labels/${String(id)}`, {
|
|
4859
4970
|
method: "DELETE",
|
|
4860
|
-
headers: authHeaders
|
|
4971
|
+
headers: authHeaders(conn)
|
|
4861
4972
|
}, { idempotent: true });
|
|
4862
4973
|
if (!res.ok) await throwForgejoFailure(res, {
|
|
4863
4974
|
verb: "remove label",
|
|
@@ -5521,75 +5632,45 @@ function buildReport(detail, checks, ctx, extras = {}) {
|
|
|
5521
5632
|
return report;
|
|
5522
5633
|
}
|
|
5523
5634
|
async function waitForSettledChecks(fetcher, target, baseline, notices, poll) {
|
|
5524
|
-
const { timeoutMs, intervalMs, sleep, now } = poll;
|
|
5525
|
-
const startedAt = now();
|
|
5526
5635
|
let currentDetail = target.detail;
|
|
5527
|
-
let consecutiveFailures = 0;
|
|
5528
5636
|
let consecutiveDetailFailures = 0;
|
|
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
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
if (now() - startedAt >= timeoutMs) return buildReport(currentDetail, lastGoodChecks ?? EMPTY_CHECKS, {
|
|
5565
|
-
target,
|
|
5566
|
-
baseline,
|
|
5567
|
-
notices,
|
|
5568
|
-
waited: true,
|
|
5569
|
-
elapsedMs: now() - startedAt
|
|
5570
|
-
}, { statusOverride: "timeout" });
|
|
5571
|
-
await sleep(intervalMs);
|
|
5572
|
-
let nextDetail;
|
|
5573
|
-
try {
|
|
5574
|
-
nextDetail = await getForgejoPrDetail(fetcher, target.conn, target.prNumber);
|
|
5575
|
-
consecutiveDetailFailures = 0;
|
|
5576
|
-
} catch (error) {
|
|
5577
|
-
consecutiveDetailFailures += 1;
|
|
5578
|
-
if (consecutiveDetailFailures >= 3) throw error;
|
|
5579
|
-
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";
|
|
5580
5672
|
}
|
|
5581
|
-
|
|
5582
|
-
target,
|
|
5583
|
-
baseline,
|
|
5584
|
-
notices,
|
|
5585
|
-
waited: true,
|
|
5586
|
-
elapsedMs: now() - startedAt
|
|
5587
|
-
}, {
|
|
5588
|
-
statusOverride: "head-moved",
|
|
5589
|
-
previousHeadSha: sha
|
|
5590
|
-
});
|
|
5591
|
-
currentDetail = nextDetail;
|
|
5592
|
-
}
|
|
5673
|
+
});
|
|
5593
5674
|
}
|
|
5594
5675
|
async function runPrChecks(fetcher, options) {
|
|
5595
5676
|
const timings = options.wait === true ? resolvePollTimings(options) : void 0;
|
|
@@ -5622,13 +5703,7 @@ function baselineClause(report) {
|
|
|
5622
5703
|
return `${String(redChecks.length)} red, ${String(preexistingCount)} of them already red on ` + report.baseline.ref;
|
|
5623
5704
|
}
|
|
5624
5705
|
function renderPrChecks(report) {
|
|
5625
|
-
const lines = report.checks.map((check) =>
|
|
5626
|
-
const parts = [check.state, check.context];
|
|
5627
|
-
if (check.description !== null) parts.push(check.description);
|
|
5628
|
-
let line = parts.join(" -- ");
|
|
5629
|
-
if (check.verdict === "red" && check.targetUrl !== null) line += ` (${check.targetUrl})`;
|
|
5630
|
-
return line;
|
|
5631
|
-
});
|
|
5706
|
+
const lines = report.checks.map((check) => renderCheckLine(check));
|
|
5632
5707
|
const summaryParts = [`head ${report.headSha.slice(0, 12)}: ${report.status}`];
|
|
5633
5708
|
const clause = baselineClause(report);
|
|
5634
5709
|
if (clause !== null) summaryParts.push(clause);
|
|
@@ -6543,14 +6618,9 @@ function parseState(state) {
|
|
|
6543
6618
|
if (state === "open" || state === "closed" || state === "all") return state;
|
|
6544
6619
|
throw new FatalError(`Invalid --state "${state}" (expected open, closed, or all).`);
|
|
6545
6620
|
}
|
|
6546
|
-
function parseLimit(raw) {
|
|
6547
|
-
const limit = Number(raw);
|
|
6548
|
-
if (!Number.isInteger(limit) || limit < 1) throw new FatalError(`Invalid --limit "${raw}" (expected a positive integer).`);
|
|
6549
|
-
return limit;
|
|
6550
|
-
}
|
|
6551
6621
|
async function runPrList(fetcher, options) {
|
|
6552
6622
|
const state = parseState(options.state);
|
|
6553
|
-
const limit =
|
|
6623
|
+
const limit = parseLimitFlag(options.limit);
|
|
6554
6624
|
rootLogger.debug(`pr:list state=${state} limit=${String(limit)}`);
|
|
6555
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.");
|
|
6556
6626
|
const notices = [];
|