@bensandee/forgejo-cli 0.4.3 → 0.4.4
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 +99 -29
- package/package.json +1 -1
package/dist/bin.mjs
CHANGED
|
@@ -163,7 +163,7 @@ function loadEnvFileForBin(rawArgs, bin, io = processIo) {
|
|
|
163
163
|
//#region src/version.ts
|
|
164
164
|
const parsed = z.object({ version: z.string() }).safeParse({
|
|
165
165
|
name: "@bensandee/forgejo-cli",
|
|
166
|
-
version: "0.4.
|
|
166
|
+
version: "0.4.4",
|
|
167
167
|
description: "bst-fj: Forgejo API commands without the rest of bst",
|
|
168
168
|
license: "MIT",
|
|
169
169
|
repository: {
|
|
@@ -2337,8 +2337,8 @@ async function createRelease(executor, conn, tag, options = {}) {
|
|
|
2337
2337
|
const url = `${conn.serverUrl}/api/v1/repos/${conn.repository}/releases`;
|
|
2338
2338
|
const payload = {
|
|
2339
2339
|
tag_name: tag,
|
|
2340
|
-
name: tag,
|
|
2341
|
-
body: `Published ${tag}`,
|
|
2340
|
+
name: options.name ?? tag,
|
|
2341
|
+
body: options.body ?? `Published ${tag}`,
|
|
2342
2342
|
prerelease: options.prerelease ?? false
|
|
2343
2343
|
};
|
|
2344
2344
|
if (options.target) payload["target_commitish"] = options.target;
|
|
@@ -2434,7 +2434,9 @@ async function ensureRelease(executor, conn, tag, options = {}) {
|
|
|
2434
2434
|
try {
|
|
2435
2435
|
await createRelease(executor, conn, tag, {
|
|
2436
2436
|
target: options.target,
|
|
2437
|
-
prerelease: options.prerelease
|
|
2437
|
+
prerelease: options.prerelease,
|
|
2438
|
+
name: options.name,
|
|
2439
|
+
body: options.body
|
|
2438
2440
|
});
|
|
2439
2441
|
} catch (error) {
|
|
2440
2442
|
const raced = await getReleaseByTag(executor, conn, tag);
|
|
@@ -2458,6 +2460,8 @@ async function ensureRelease(executor, conn, tag, options = {}) {
|
|
|
2458
2460
|
const recreated = options.target ? await recreateTagViaApi(executor, conn, tag, {
|
|
2459
2461
|
target: options.target,
|
|
2460
2462
|
prerelease: options.prerelease,
|
|
2463
|
+
name: options.name,
|
|
2464
|
+
body: options.body,
|
|
2461
2465
|
current,
|
|
2462
2466
|
attempts,
|
|
2463
2467
|
baseDelayMs
|
|
@@ -2469,7 +2473,7 @@ async function ensureRelease(executor, conn, tag, options = {}) {
|
|
|
2469
2473
|
throw new TransientError(`Release for ${tag} never got a real (non-epoch) created date after ${String(attempts)} attempts. This is an intermittent Forgejo bug: a pushed tag's date settles asynchronously and a racing release creation freezes it at the Unix epoch (1969/1970). ` + (options.target ? `Recreating the tag via the API did not help -- investigate the server or upgrade Forgejo.` : `Pass the release tag's target commit so the tag can be recreated, or retry the release.`));
|
|
2470
2474
|
}
|
|
2471
2475
|
async function recreateTagViaApi(executor, conn, tag, opts) {
|
|
2472
|
-
const { target, prerelease, current, attempts, baseDelayMs } = opts;
|
|
2476
|
+
const { target, prerelease, name, body, current, attempts, baseDelayMs } = opts;
|
|
2473
2477
|
log.warn(`Release for ${tag} stayed epoch-dated -- recreating the tag via the API so Forgejo stamps a real date (the tag becomes lightweight)`);
|
|
2474
2478
|
if (current) await deleteRelease(executor, conn, current.id);
|
|
2475
2479
|
await deleteTag(executor, conn, tag);
|
|
@@ -2477,7 +2481,9 @@ async function recreateTagViaApi(executor, conn, tag, opts) {
|
|
|
2477
2481
|
try {
|
|
2478
2482
|
await createRelease(executor, conn, tag, {
|
|
2479
2483
|
target,
|
|
2480
|
-
prerelease
|
|
2484
|
+
prerelease,
|
|
2485
|
+
name,
|
|
2486
|
+
body
|
|
2481
2487
|
});
|
|
2482
2488
|
} catch (error) {
|
|
2483
2489
|
if (attempt >= attempts) throw new TransientError(`Release for ${tag} was deleted to repair its epoch date, but recreating it from ${target} failed: ${describeError(error)}. The tag no longer exists on the server -- re-push it (or re-run the release) to restore it.`);
|
|
@@ -2501,6 +2507,24 @@ async function settleBackoff(attempt, baseDelayMs) {
|
|
|
2501
2507
|
const delay = Math.min(baseDelayMs * 2 ** (attempt - 1), SETTLE_MAX_DELAY_MS);
|
|
2502
2508
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2503
2509
|
}
|
|
2510
|
+
z.looseObject({
|
|
2511
|
+
id: z.number(),
|
|
2512
|
+
tag_name: z.string(),
|
|
2513
|
+
name: z.string().nullish(),
|
|
2514
|
+
target_commitish: z.string().nullish(),
|
|
2515
|
+
created_at: z.string().nullish(),
|
|
2516
|
+
html_url: z.string().nullish(),
|
|
2517
|
+
draft: z.boolean().optional(),
|
|
2518
|
+
prerelease: z.boolean().optional(),
|
|
2519
|
+
assets: z.array(z.unknown()).nullish()
|
|
2520
|
+
});
|
|
2521
|
+
z.looseObject({
|
|
2522
|
+
name: z.string(),
|
|
2523
|
+
commit: z.looseObject({
|
|
2524
|
+
sha: z.string(),
|
|
2525
|
+
created: z.string().nullish()
|
|
2526
|
+
})
|
|
2527
|
+
});
|
|
2504
2528
|
//#endregion
|
|
2505
2529
|
//#region src/commands/release-create.ts
|
|
2506
2530
|
const releaseCreateCommand = defineCommand({
|
|
@@ -6565,36 +6589,52 @@ z.object({
|
|
|
6565
6589
|
})
|
|
6566
6590
|
});
|
|
6567
6591
|
//#endregion
|
|
6568
|
-
//#region src/pr-merge.ts
|
|
6569
|
-
const
|
|
6592
|
+
//#region ../forgejo/src/pr-merge-run.ts
|
|
6593
|
+
const MERGE_METHODS = [
|
|
6570
6594
|
"merge",
|
|
6571
6595
|
"rebase",
|
|
6572
6596
|
"squash"
|
|
6573
6597
|
];
|
|
6574
6598
|
const RECHECK_DELAY_MS = 2e3;
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6599
|
+
const MIN_SHA_PREFIX = 7;
|
|
6600
|
+
function sleepDefault(ms) {
|
|
6601
|
+
return new Promise((resolve) => {
|
|
6602
|
+
setTimeout(resolve, ms);
|
|
6603
|
+
});
|
|
6580
6604
|
}
|
|
6581
|
-
|
|
6582
|
-
const
|
|
6583
|
-
|
|
6584
|
-
|
|
6605
|
+
function headMatches(expected, actual) {
|
|
6606
|
+
const want = expected.trim().toLowerCase();
|
|
6607
|
+
return want.length >= MIN_SHA_PREFIX && actual.toLowerCase().startsWith(want);
|
|
6608
|
+
}
|
|
6609
|
+
async function runPrMergeCore(fetcher, conn, number, options = {}) {
|
|
6585
6610
|
const pr = `PR #${String(number)}`;
|
|
6611
|
+
const method = options.method ?? "merge";
|
|
6612
|
+
const expected = options.expectedHeadSha;
|
|
6613
|
+
const moved = (headSha) => expected !== void 0 && !headMatches(expected, headSha);
|
|
6586
6614
|
const confirmation = await confirmMergeable(fetcher, conn, number, {
|
|
6587
6615
|
delayMs: RECHECK_DELAY_MS,
|
|
6588
|
-
sleep: options.sleep ??
|
|
6589
|
-
settleOnFirstRead: (gate) => !gate.open || gate.draft
|
|
6616
|
+
sleep: options.sleep ?? sleepDefault,
|
|
6617
|
+
settleOnFirstRead: (gate) => !gate.open || gate.draft || moved(gate.headSha)
|
|
6590
6618
|
});
|
|
6591
6619
|
const { gate, verdict } = confirmation;
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
if (
|
|
6620
|
+
const refuse = (why) => ({
|
|
6621
|
+
kind: "refused",
|
|
6622
|
+
reason: `Refusing to merge ${pr}: ${why}`
|
|
6623
|
+
});
|
|
6624
|
+
if (!gate.open) return refuse("it is closed.");
|
|
6625
|
+
if (gate.draft) return refuse("it is a draft.");
|
|
6626
|
+
if (options.refuseEmpty === true && (gate.headSha === gate.mergeBase || gate.changedFiles === 0)) return refuse("its diff against its base is empty, so its branch is left alone.");
|
|
6627
|
+
if (expected !== void 0 && moved(gate.headSha)) return refuse(`its head is ${gate.headSha}, not the approved ${expected}.`);
|
|
6628
|
+
if (verdict === "conflicted") return refuse(`it has conflicts. ${confirmation.evidence ?? ""}`.trimEnd());
|
|
6629
|
+
if (verdict === "unconfirmed") return refuse(`the forge could not confirm it is mergeable. ${confirmation.evidence ?? ""}`.trimEnd());
|
|
6630
|
+
const read = await readCommitChecks(fetcher, conn, gate.headSha);
|
|
6631
|
+
const checks = classifyBaseStatus(read);
|
|
6632
|
+
const skippedOnly = read.parsed && read.contexts.length > 0 && read.contexts.every((context) => context.verdict === "skipped");
|
|
6633
|
+
if (!(checks === "green" || options.ignoreChecks === true || (checks === "no-status" || skippedOnly) && options.allowNoStatus === true)) return {
|
|
6634
|
+
kind: "refused",
|
|
6635
|
+
reason: `Refusing to merge ${pr}: its checks at ${gate.headSha} are ${checks}, not green.`,
|
|
6636
|
+
checks
|
|
6637
|
+
};
|
|
6598
6638
|
const result = {
|
|
6599
6639
|
number,
|
|
6600
6640
|
merged: false,
|
|
@@ -6603,17 +6643,47 @@ async function runPrMerge(fetcher, options) {
|
|
|
6603
6643
|
verdict,
|
|
6604
6644
|
checks
|
|
6605
6645
|
};
|
|
6606
|
-
if (options.dryRun) return
|
|
6646
|
+
if (options.dryRun === true) return {
|
|
6647
|
+
kind: "ok",
|
|
6648
|
+
result
|
|
6649
|
+
};
|
|
6607
6650
|
await mergePr(fetcher, conn, number, {
|
|
6608
6651
|
method,
|
|
6609
|
-
deleteBranch: options.deleteBranch,
|
|
6652
|
+
deleteBranch: options.deleteBranch ?? false,
|
|
6610
6653
|
expectedHeadSha: gate.headSha
|
|
6611
6654
|
});
|
|
6612
6655
|
return {
|
|
6613
|
-
|
|
6614
|
-
|
|
6656
|
+
kind: "ok",
|
|
6657
|
+
result: {
|
|
6658
|
+
...result,
|
|
6659
|
+
merged: true
|
|
6660
|
+
}
|
|
6615
6661
|
};
|
|
6616
6662
|
}
|
|
6663
|
+
//#endregion
|
|
6664
|
+
//#region src/pr-merge.ts
|
|
6665
|
+
function parseMethod(raw) {
|
|
6666
|
+
if (raw === void 0) return "merge";
|
|
6667
|
+
const method = MERGE_METHODS.find((candidate) => candidate === raw);
|
|
6668
|
+
if (method === void 0) throw new FatalError(`Invalid --method "${raw}" (expected one of: ${MERGE_METHODS.join(", ")}).`);
|
|
6669
|
+
return method;
|
|
6670
|
+
}
|
|
6671
|
+
async function runPrMerge(fetcher, options) {
|
|
6672
|
+
const number = parsePrNumber(options.number);
|
|
6673
|
+
const method = parseMethod(options.method);
|
|
6674
|
+
const outcome = await runPrMergeCore(fetcher, requireForgejoTarget(options.cwd, parseRepoTarget(options.repo), "pr:merge is Forgejo-only -- use gh pr merge on GitHub."), number, {
|
|
6675
|
+
method,
|
|
6676
|
+
deleteBranch: options.deleteBranch,
|
|
6677
|
+
ignoreChecks: options.ignoreChecks,
|
|
6678
|
+
dryRun: options.dryRun,
|
|
6679
|
+
sleep: options.sleep ?? sleep
|
|
6680
|
+
});
|
|
6681
|
+
if (outcome.kind === "refused") {
|
|
6682
|
+
const hint = outcome.checks === void 0 ? "" : " Pass --ignore-checks to merge anyway.";
|
|
6683
|
+
throw new FatalError(`${outcome.reason}${hint}`);
|
|
6684
|
+
}
|
|
6685
|
+
return outcome.result;
|
|
6686
|
+
}
|
|
6617
6687
|
function renderPrMerge(result) {
|
|
6618
6688
|
const what = `PR #${String(result.number)} (${result.method}) at ${result.headSha.slice(0, 12)}, checks ${result.checks}`;
|
|
6619
6689
|
return result.merged ? `Merged ${what}.` : `Dry run: would merge ${what}.`;
|