@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.
- package/README.md +1 -1
- package/dist/cli.js +97 -26
- 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|[
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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 (
|
|
3082
|
-
|
|
3083
|
-
|
|
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
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
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.
|
|
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",
|