@kaynooo/forgejo-tools 0.8.0 → 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 +62 -7
  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 show one line per run, redrawn in place on a terminal and left at its last status, 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.
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.8.0";
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) {
@@ -3068,7 +3101,8 @@ var STALLED2 = 3 * 60000;
3068
3101
  async function watchRuns(pick, timeout, repo, idle, out = process.stdout) {
3069
3102
  const started = Date.now();
3070
3103
  const latest = new Map;
3071
- const line = (run) => `${run.workflow_id} run ${run.id} on ${run.prettyref}: ${badge(run.status, out)}`;
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())}`;
3072
3106
  const region = takesControlCodes(out) ? liveRegion(() => [...latest.values()].map(line), out) : undefined;
3073
3107
  const followed = new Set;
3074
3108
  const failed = new Set;
@@ -3080,6 +3114,8 @@ async function watchRuns(pick, timeout, repo, idle, out = process.stdout) {
3080
3114
  for (const run of runs)
3081
3115
  followed.add(run.id);
3082
3116
  for (const run of runs) {
3117
+ if (!seen.has(run.id))
3118
+ seen.set(run.id, Date.now());
3083
3119
  if (region === undefined && latest.get(run.id)?.status !== run.status)
3084
3120
  out.write(`${line(run)}
3085
3121
  `);
@@ -3143,6 +3179,12 @@ ${run.html_url}`);
3143
3179
  }
3144
3180
  }
3145
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
+ }
3146
3188
  async function watch(args) {
3147
3189
  const parsed = parse(args, usage4, options5);
3148
3190
  const { values } = parsed;
@@ -3186,6 +3228,7 @@ var REGISTRY_POLL = 5000;
3186
3228
  var limits = { pushMs: 5 * 60000 };
3187
3229
  var REGISTRY_INFO_MS = 30000;
3188
3230
  var NO_RUN2 = 3 * 60000;
3231
+ var RUNS_LOOKED_AT = 20;
3189
3232
  function taggedVersion(tag, prefix) {
3190
3233
  const version = tag.slice(prefix.length);
3191
3234
  return tag.startsWith(prefix) && VERSION.test(version) ? version : undefined;
@@ -3250,6 +3293,14 @@ async function waitForRegistry(name, version) {
3250
3293
  }
3251
3294
  return false;
3252
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
+ }
3253
3304
  async function testsPass(sha, branch, timeout) {
3254
3305
  if (!await gitOk(["merge-base", "--is-ancestor", sha, `origin/${branch}`])) {
3255
3306
  const held = holdSignals();
@@ -3265,6 +3316,10 @@ async function testsPass(sha, branch, timeout) {
3265
3316
  fail(`origin refused the push of ${sha.slice(0, 7)}, so nothing was bumped:
3266
3317
  ${push.stderr.trim()}`, 1);
3267
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`);
3268
3323
  }
3269
3324
  return await watchRuns((runs) => newestPerWorkflow(runs.filter((run) => run.commit_sha === sha)), timeout, undefined, NO_RUN2) === 0;
3270
3325
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kaynooo/forgejo-tools",
3
3
  "type": "module",
4
- "version": "0.8.0",
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",