@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.
Files changed (2) hide show
  1. package/dist/bin.mjs +99 -29
  2. 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.3",
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 METHODS = [
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
- function parseMethod(raw) {
6576
- if (raw === void 0) return "merge";
6577
- const method = METHODS.find((candidate) => candidate === raw);
6578
- if (method === void 0) throw new FatalError(`Invalid --method "${raw}" (expected one of: ${METHODS.join(", ")}).`);
6579
- return method;
6599
+ const MIN_SHA_PREFIX = 7;
6600
+ function sleepDefault(ms) {
6601
+ return new Promise((resolve) => {
6602
+ setTimeout(resolve, ms);
6603
+ });
6580
6604
  }
6581
- async function runPrMerge(fetcher, options) {
6582
- const number = parsePrNumber(options.number);
6583
- const method = parseMethod(options.method);
6584
- const conn = requireForgejoTarget(options.cwd, parseRepoTarget(options.repo), "pr:merge is Forgejo-only -- use gh pr merge on GitHub.");
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 ?? 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
- if (!gate.open) throw new FatalError(`Refusing to merge ${pr}: it is closed.`);
6593
- if (gate.draft) throw new FatalError(`Refusing to merge ${pr}: it is a draft.`);
6594
- if (verdict === "conflicted") throw new FatalError(`Refusing to merge ${pr}: it has conflicts. ${confirmation.evidence ?? ""}`.trimEnd());
6595
- if (verdict === "unconfirmed") throw new FatalError(`Refusing to merge ${pr}: the forge could not confirm it is mergeable. ${confirmation.evidence ?? ""}`.trimEnd());
6596
- const checks = classifyBaseStatus(await readCommitChecks(fetcher, conn, gate.headSha));
6597
- if (checks !== "green" && !options.ignoreChecks) throw new FatalError(`Refusing to merge ${pr}: its checks at ${gate.headSha} are ${checks}, not green. Pass --ignore-checks to merge anyway.`);
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 result;
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
- ...result,
6614
- merged: true
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}.`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/forgejo-cli",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "bst-fj: Forgejo API commands without the rest of bst",
5
5
  "license": "MIT",
6
6
  "repository": {