@kaynooo/forgejo-tools 0.7.1 → 0.9.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +97 -26
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -33,7 +33,7 @@ Needs [tea](https://gitea.com/gitea/tea) logged in to the Forgejo instance (`tea
33
33
  | `ft milestone <title\|id>` | shows a milestone's open issues as `ft board` does (available, taken, claimed by one gone); `ft milestone new <title> [-d text] [--due YYYY-MM-DD]` creates one through the API, sending no `due_on` unless `--due` is given, never a zero date; `ft milestone close <m>` refuses while issues are open in it, naming them, unless `--force` |
34
34
  | `ft remote [--fix]` | switches an origin using an ssh alias (`forgejo:owner/repo`) to `git@<host>:owner/repo.git`, which tea matches; the host comes from `--host` or `remoteHost` in `~/.config/ft/config.json` (`{"remoteHost": "git.example.com"}`), and with neither it checks nothing |
35
35
 
36
- `ft <command> --help` lists each command's options. Waiting commands exit 0 when the runs pass, 1 when one fails, 2 on a timeout, a stalled queue or bad usage; the other commands exit 0 when done, 1 when Forgejo refuses or a check fails, 2 on a usage error.
36
+ `ft <command> --help` lists each command's options. Waiting commands show one line per run with how long it has been waiting or running (`● running for 1m 12s`), redrawn in place on a terminal and left at its last status with the time it took (`✓ success in 3m 2s`), and a line per status change in a log or a pipe; they exit 0 when the runs pass, 1 when one fails, 2 on a timeout, a stalled queue or bad usage; the other commands exit 0 when done, 1 when Forgejo refuses or a check fails, 2 on a usage error.
37
37
 
38
38
  ## CI
39
39
 
package/dist/cli.js CHANGED
@@ -599,15 +599,15 @@ function wholeNumber(value, name, fallback, min = 1) {
599
599
  badUsage(`${name.startsWith("<") ? name : `--${name}`} takes a whole number from ${min}, not ${value}`);
600
600
  return number;
601
601
  }
602
- var DURATION = /^(\d+(?:\.\d+)?)(ms|[smh])?$/;
603
- var UNIT_MS = { ms: 1, s: 1000, m: 60000, h: 3600000 };
602
+ var DURATION = /^(\d+(?:\.\d+)?)(ms|[smhd])?$/;
603
+ var UNIT_MS = { ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 };
604
604
  function duration(value, name, fallback, unit = "s") {
605
605
  if (value === undefined)
606
606
  return fallback;
607
607
  const match = DURATION.exec(value.trim());
608
608
  const ms = match === null ? 0 : Number(match[1]) * UNIT_MS[match[2] ?? unit];
609
609
  if (ms < 1)
610
- badUsage(`--${name} takes a duration such as 30s, 5m or 1h, not ${value}`);
610
+ badUsage(`--${name} takes a duration such as 30s, 5m, 1h or 7d, not ${value}`);
611
611
  return ms;
612
612
  }
613
613
  async function readInput(path, { noStdin = "nothing is piped on stdin" } = {}) {
@@ -754,6 +754,7 @@ var LINE_UP = "\x1B[1A\x1B[2K";
754
754
  var ESC = String.fromCharCode(27);
755
755
  var ESCAPE_CODE = new RegExp(`${ESC}(?:\\[[\\x30-\\x3F]*[\\x20-\\x2F]*[\\x40-\\x7E]|\\][^\\u0007${ESC}]*(?:\\u0007|${ESC}\\\\))`, "y");
756
756
  var RESET_STYLE = `${ESC}[0m`;
757
+ var CTRL_C = 3;
757
758
  var CLOSE_LINK = `${ESC}]8;;${ESC}\\`;
758
759
  var CONSOLE_METHODS = ["log", "info", "warn", "error", "debug", "dir", "dirxml", "table", "trace", "group", "groupCollapsed", "count", "timeEnd", "timeLog", "assert"];
759
760
  function liveRegion(render, stream = process6.stderr, env = process6.env) {
@@ -786,12 +787,17 @@ function liveRegion(render, stream = process6.stderr, env = process6.env) {
786
787
  };
787
788
  const timer = setInterval(draw, FRAME_MS);
788
789
  const restore = interceptOutput(clear);
789
- const onExit = () => clear();
790
+ const release = swallowInput();
791
+ const onExit = () => {
792
+ clear();
793
+ release();
794
+ };
790
795
  const signals = holdSignals(stop);
791
796
  function stop() {
792
797
  clearInterval(timer);
793
798
  clear();
794
799
  restore();
800
+ release();
795
801
  process6.off("exit", onExit);
796
802
  signals.release();
797
803
  }
@@ -834,6 +840,29 @@ function cutToWidth(line, width) {
834
840
  }
835
841
  return `${kept}${RESET_STYLE}${linked ? CLOSE_LINK : ""}`;
836
842
  }
843
+ function swallowInput() {
844
+ const input = process6.stdin;
845
+ if (!input.isTTY)
846
+ return () => {
847
+ return;
848
+ };
849
+ const onData = (data) => {
850
+ if (data.includes(CTRL_C))
851
+ process6.kill(process6.pid, "SIGINT");
852
+ };
853
+ input.setRawMode(true);
854
+ input.on("data", onData);
855
+ input.resume();
856
+ let released = false;
857
+ return () => {
858
+ if (released)
859
+ return;
860
+ released = true;
861
+ input.off("data", onData);
862
+ input.pause();
863
+ input.setRawMode(false);
864
+ };
865
+ }
837
866
  function interceptOutput(clear) {
838
867
  const targets = [...CONSOLE_METHODS.map((name) => [console, name]), [process6.stdout, "write"], [process6.stderr, "write"]];
839
868
  const wrapped = targets.map(([target, name]) => {
@@ -1292,7 +1321,7 @@ async function currentSession(sessions) {
1292
1321
  return;
1293
1322
  }
1294
1323
  // package.json
1295
- var version = "0.7.1";
1324
+ var version = "0.9.0";
1296
1325
 
1297
1326
  // src/args.ts
1298
1327
  var repoOption = {
@@ -1980,8 +2009,12 @@ var EPOCH = Date.parse("2000-01-01T00:00:00Z");
1980
2009
  function runSeconds(run) {
1981
2010
  return run.duration / NANOSECONDS;
1982
2011
  }
2012
+ function startedAt(run) {
2013
+ const at = Date.parse(run.started);
2014
+ return Number.isNaN(at) || at < EPOCH ? undefined : at;
2015
+ }
1983
2016
  function startedAge(run, now = Date.now()) {
1984
- return Date.parse(run.started) < EPOCH ? "-" : formatAge(run.started, now);
2017
+ return startedAt(run) === undefined ? "-" : formatAge(run.started, now);
1985
2018
  }
1986
2019
  var RUNS_PAGE = 50;
1987
2020
  async function runsListing(limit, repo, workflow) {
@@ -3065,9 +3098,12 @@ var options5 = {
3065
3098
  var timing = { poll: 1e4 };
3066
3099
  var LIMIT = 20;
3067
3100
  var STALLED2 = 3 * 60000;
3068
- async function watchRuns(pick, timeout, repo, idle) {
3101
+ async function watchRuns(pick, timeout, repo, idle, out = process.stdout) {
3069
3102
  const started = Date.now();
3070
- const reported = new Map;
3103
+ const latest = new Map;
3104
+ const seen = new Map;
3105
+ const line = (run) => `${run.workflow_id} run ${run.id} on ${run.prettyref}: ${badge(run.status, out)}${elapsed(run, seen.get(run.id) ?? Date.now())}`;
3106
+ const region = takesControlCodes(out) ? liveRegion(() => [...latest.values()].map(line), out) : undefined;
3071
3107
  const followed = new Set;
3072
3108
  const failed = new Set;
3073
3109
  let moving = started;
@@ -3078,10 +3114,14 @@ async function watchRuns(pick, timeout, repo, idle) {
3078
3114
  for (const run of runs)
3079
3115
  followed.add(run.id);
3080
3116
  for (const run of runs) {
3081
- if (reported.get(run.id) !== run.status)
3082
- console.log(`${run.workflow_id} run ${run.id} on ${run.prettyref}: ${badge(run.status, process.stdout)}`);
3083
- reported.set(run.id, run.status);
3117
+ if (!seen.has(run.id))
3118
+ seen.set(run.id, Date.now());
3119
+ if (region === undefined && latest.get(run.id)?.status !== run.status)
3120
+ out.write(`${line(run)}
3121
+ `);
3122
+ latest.set(run.id, run);
3084
3123
  }
3124
+ region?.redraw();
3085
3125
  if (runs.length === 0) {
3086
3126
  if (idle !== undefined && Date.now() - started > idle) {
3087
3127
  console.log("no run started, so nothing was waited for");
@@ -3109,24 +3149,42 @@ ${run.html_url}`);
3109
3149
  fail(`queued runs are not moving: ${stall.reason}`);
3110
3150
  return;
3111
3151
  }
3112
- for (;; ) {
3113
- await Bun.sleep(timing.poll);
3114
- if (Date.now() - started > timeout)
3115
- fail(`the runs did not finish in ${formatDuration(timeout)}`);
3116
- try {
3117
- const code = await poll();
3118
- if (code !== undefined)
3119
- return code;
3120
- } catch (error) {
3121
- if (error instanceof Exit)
3122
- throw error;
3123
- const message = error instanceof Error ? error.message : String(error);
3124
- if (isPermanentFailure(error))
3125
- fail(message);
3126
- console.warn(`ft watch: ${message}, polling again`);
3152
+ async function settle() {
3153
+ for (;; ) {
3154
+ await Bun.sleep(timing.poll);
3155
+ if (Date.now() - started > timeout)
3156
+ fail(`the runs did not finish in ${formatDuration(timeout)}`);
3157
+ try {
3158
+ const code = await poll();
3159
+ if (code !== undefined)
3160
+ return code;
3161
+ } catch (error) {
3162
+ if (error instanceof Exit)
3163
+ throw error;
3164
+ const message = error instanceof Error ? error.message : String(error);
3165
+ if (isPermanentFailure(error))
3166
+ fail(message);
3167
+ console.warn(`ft watch: ${message}, polling again`);
3168
+ }
3169
+ }
3170
+ }
3171
+ try {
3172
+ return await settle();
3173
+ } finally {
3174
+ if (region !== undefined) {
3175
+ region.stop();
3176
+ for (const run of latest.values())
3177
+ out.write(`${line(run)}
3178
+ `);
3127
3179
  }
3128
3180
  }
3129
3181
  }
3182
+ function elapsed(run, seen) {
3183
+ if (FINISHED.has(run.status))
3184
+ return run.duration > 0 ? ` in ${formatSeconds(runSeconds(run))}` : "";
3185
+ const seconds = Math.floor((Date.now() - (startedAt(run) ?? seen)) / 1000);
3186
+ return seconds > 0 ? ` for ${formatSeconds(seconds)}` : "";
3187
+ }
3130
3188
  async function watch(args) {
3131
3189
  const parsed = parse(args, usage4, options5);
3132
3190
  const { values } = parsed;
@@ -3170,6 +3228,7 @@ var REGISTRY_POLL = 5000;
3170
3228
  var limits = { pushMs: 5 * 60000 };
3171
3229
  var REGISTRY_INFO_MS = 30000;
3172
3230
  var NO_RUN2 = 3 * 60000;
3231
+ var RUNS_LOOKED_AT = 20;
3173
3232
  function taggedVersion(tag, prefix) {
3174
3233
  const version = tag.slice(prefix.length);
3175
3234
  return tag.startsWith(prefix) && VERSION.test(version) ? version : undefined;
@@ -3234,6 +3293,14 @@ async function waitForRegistry(name, version) {
3234
3293
  }
3235
3294
  return false;
3236
3295
  }
3296
+ async function alreadyPassed(sha) {
3297
+ try {
3298
+ const own = newestPerWorkflow((await recentRuns(RUNS_LOOKED_AT)).filter((run) => run.commit_sha === sha));
3299
+ return own.length > 0 && own.every((run) => FINISHED.has(run.status)) && ["success", "skipped"].includes(overall(own));
3300
+ } catch {
3301
+ return false;
3302
+ }
3303
+ }
3237
3304
  async function testsPass(sha, branch, timeout) {
3238
3305
  if (!await gitOk(["merge-base", "--is-ancestor", sha, `origin/${branch}`])) {
3239
3306
  const held = holdSignals();
@@ -3249,6 +3316,10 @@ async function testsPass(sha, branch, timeout) {
3249
3316
  fail(`origin refused the push of ${sha.slice(0, 7)}, so nothing was bumped:
3250
3317
  ${push.stderr.trim()}`, 1);
3251
3318
  console.log(`pushed ${branch} at ${sha.slice(0, 7)}, waiting for its runs before the bump`);
3319
+ } else if (await alreadyPassed(sha)) {
3320
+ return true;
3321
+ } else {
3322
+ console.log(`${branch} at ${sha.slice(0, 7)} is on origin, waiting for its runs before the bump`);
3252
3323
  }
3253
3324
  return await watchRuns((runs) => newestPerWorkflow(runs.filter((run) => run.commit_sha === sha)), timeout, undefined, NO_RUN2) === 0;
3254
3325
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kaynooo/forgejo-tools",
3
3
  "type": "module",
4
- "version": "0.7.1",
4
+ "version": "0.9.0",
5
5
  "description": "ft: Forgejo CI runs, logs, releases and issue checklists from the terminal, through tea.",
6
6
  "author": "Kaynooo <contact@kaynooo.fr>",
7
7
  "license": "MIT",