@bensandee/forgejo-cli 0.2.0 → 0.3.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 +2 -0
- package/dist/bin.mjs +784 -106
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,6 +55,8 @@ command takes `--json`, which prints exactly one JSON document, failures include
|
|
|
55
55
|
| `pr:files` | List the files a pull request changes |
|
|
56
56
|
| `pr:merge` | Merge an open, non-draft, mergeable PR pinned to the head it read; refuses closed, draft, conflicted, unconfirmed or non-green checks (zero statuses included) unless `--ignore-checks`; `--delete-branch` is opt-in |
|
|
57
57
|
| `workflow:dispatch` | Dispatch a workflow with `--workflow`, `--ref` and repeatable `--input`; takes `--repo owner/repo`, not a URL |
|
|
58
|
+
| `ci:runs` | List a repository's workflow runs with their jobs, filtered by workflow, ref, pull request, head SHA or status |
|
|
59
|
+
| `ci:log` | Print one workflow job's whole log, or with `--grep` only the lines whose text matches |
|
|
58
60
|
|
|
59
61
|
Zero posted statuses reads as `no-status`, never as green. Every command is Forgejo-only: on
|
|
60
62
|
GitHub, use `gh`. The `issue:*`, `pr:create` and `pr:update` commands take the same flags and print the
|
package/dist/bin.mjs
CHANGED
|
@@ -245,7 +245,7 @@ function loadEnvFileForBin(rawArgs, bin, io = processIo) {
|
|
|
245
245
|
//#region src/version.ts
|
|
246
246
|
const parsed = z.object({ version: z.string() }).safeParse({
|
|
247
247
|
name: "@bensandee/forgejo-cli",
|
|
248
|
-
version: "0.
|
|
248
|
+
version: "0.3.0",
|
|
249
249
|
description: "bst-fj: Forgejo API commands without the rest of bst",
|
|
250
250
|
license: "MIT",
|
|
251
251
|
repository: {
|
|
@@ -1039,11 +1039,17 @@ function jsonDocumentWritten() {
|
|
|
1039
1039
|
//#endregion
|
|
1040
1040
|
//#region ../cli-kit/src/path-probe.ts
|
|
1041
1041
|
/** The real {@link PathKind} probe the pre-run argument check uses. A path that exists but is not
|
|
1042
|
-
* a directory (a file, a socket) reads as `"file"`.
|
|
1042
|
+
* a directory (a file, a socket) reads as `"file"`. A path stat cannot resolve at all -- a symlink
|
|
1043
|
+
* loop, an unsearchable parent -- reads as `"missing"`: `throwIfNoEntry` covers ENOENT and ENOTDIR
|
|
1044
|
+
* only, and a stat failure here would surface as a crash where the check owes a usage error. */
|
|
1043
1045
|
function probeCliPath(value) {
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1046
|
+
try {
|
|
1047
|
+
const stat = statSync(value, { throwIfNoEntry: false });
|
|
1048
|
+
if (stat === void 0) return "missing";
|
|
1049
|
+
return stat.isDirectory() ? "directory" : "file";
|
|
1050
|
+
} catch {
|
|
1051
|
+
return "missing";
|
|
1052
|
+
}
|
|
1047
1053
|
}
|
|
1048
1054
|
//#endregion
|
|
1049
1055
|
//#region ../cli-kit/src/run.ts
|
|
@@ -2325,7 +2331,7 @@ const commitStatusCommand = defineCommand({
|
|
|
2325
2331
|
},
|
|
2326
2332
|
json: {
|
|
2327
2333
|
type: "boolean",
|
|
2328
|
-
description: "Emit
|
|
2334
|
+
description: "Emit one JSON document: sha, ref, repository, url, status (green | red | pending | no-status | unparseable | timeout), settled, combinedState, waited, elapsedMs, checks ([{context, name, state, verdict, description (or null), targetUrl (or null), updatedAt (or null)}]), detail? (unparseable only). no-status means no check was posted and is never green."
|
|
2329
2335
|
},
|
|
2330
2336
|
verbose: {
|
|
2331
2337
|
type: "boolean",
|
|
@@ -3080,7 +3086,7 @@ const listCommand$1 = defineCommand({
|
|
|
3080
3086
|
},
|
|
3081
3087
|
json: {
|
|
3082
3088
|
type: "boolean",
|
|
3083
|
-
description: "Emit
|
|
3089
|
+
description: "Emit one JSON document: repository, labels ([{id, name, color?, description?, exclusive?}])."
|
|
3084
3090
|
}
|
|
3085
3091
|
},
|
|
3086
3092
|
async run({ args, rawArgs }) {
|
|
@@ -3134,7 +3140,7 @@ const syncCommand = defineCommand({
|
|
|
3134
3140
|
},
|
|
3135
3141
|
json: {
|
|
3136
3142
|
type: "boolean",
|
|
3137
|
-
description: "Emit
|
|
3143
|
+
description: "Emit one JSON document: set, mode (apply | check | dry-run), repos ([{repository, created, updated, unchanged, omitted, stale}], every field but repository a list of label names), failures ([{repository, error}]; always present, filled only under --fleet), drifted. drifted is true when any label is missing or drifted, and sets the exit code only under --check."
|
|
3138
3144
|
}
|
|
3139
3145
|
},
|
|
3140
3146
|
async run({ args, rawArgs }) {
|
|
@@ -3174,7 +3180,7 @@ const labelsCommand = defineCommand({
|
|
|
3174
3180
|
},
|
|
3175
3181
|
args: { json: {
|
|
3176
3182
|
type: "boolean",
|
|
3177
|
-
description: "Emit
|
|
3183
|
+
description: "Emit one JSON document; the keys are the subcommand's (see labels list --help and labels sync --help)."
|
|
3178
3184
|
} },
|
|
3179
3185
|
subCommands: {
|
|
3180
3186
|
list: listCommand$1,
|
|
@@ -3504,7 +3510,7 @@ const releaseCreateCommand = defineCommand({
|
|
|
3504
3510
|
},
|
|
3505
3511
|
json: {
|
|
3506
3512
|
type: "boolean",
|
|
3507
|
-
description: "Emit
|
|
3513
|
+
description: "Emit one JSON document: status (created | exists | race | repaired), tag, target, prerelease, release ({id, url (or null)})."
|
|
3508
3514
|
}
|
|
3509
3515
|
},
|
|
3510
3516
|
async run({ args }) {
|
|
@@ -3697,7 +3703,7 @@ const setCommand = defineCommand({
|
|
|
3697
3703
|
},
|
|
3698
3704
|
json: {
|
|
3699
3705
|
type: "boolean",
|
|
3700
|
-
description: "Emit
|
|
3706
|
+
description: "Emit one JSON document: status (set | would-set), name, repository. The secret value is never included."
|
|
3701
3707
|
}
|
|
3702
3708
|
},
|
|
3703
3709
|
async run({ args }) {
|
|
@@ -3726,7 +3732,7 @@ const listCommand = defineCommand({
|
|
|
3726
3732
|
},
|
|
3727
3733
|
json: {
|
|
3728
3734
|
type: "boolean",
|
|
3729
|
-
description: "Emit
|
|
3735
|
+
description: "Emit one JSON document: repository, secrets (secret names). Values are never returned."
|
|
3730
3736
|
}
|
|
3731
3737
|
},
|
|
3732
3738
|
async run({ args }) {
|
|
@@ -3760,7 +3766,7 @@ const deleteCommand = defineCommand({
|
|
|
3760
3766
|
},
|
|
3761
3767
|
json: {
|
|
3762
3768
|
type: "boolean",
|
|
3763
|
-
description: "Emit
|
|
3769
|
+
description: "Emit one JSON document: status (deleted | would-delete), name, repository."
|
|
3764
3770
|
}
|
|
3765
3771
|
},
|
|
3766
3772
|
async run({ args }) {
|
|
@@ -3781,7 +3787,7 @@ const secretsCommand = defineCommand({
|
|
|
3781
3787
|
},
|
|
3782
3788
|
args: { json: {
|
|
3783
3789
|
type: "boolean",
|
|
3784
|
-
description: "Emit
|
|
3790
|
+
description: "Emit one JSON document; the keys are the subcommand's (see secrets set --help, secrets list --help and secrets delete --help)."
|
|
3785
3791
|
} },
|
|
3786
3792
|
subCommands: {
|
|
3787
3793
|
set: setCommand,
|
|
@@ -3986,7 +3992,7 @@ const webhooksCommand = defineCommand({
|
|
|
3986
3992
|
},
|
|
3987
3993
|
args: { json: {
|
|
3988
3994
|
type: "boolean",
|
|
3989
|
-
description: "Emit
|
|
3995
|
+
description: "Emit one JSON document: user, isAdmin, groups ([{label, hooks ([{id, type, active, config?}])}]), totalCount."
|
|
3990
3996
|
} },
|
|
3991
3997
|
async run({ args }) {
|
|
3992
3998
|
const conn = requireForgejoTarget(process.cwd(), void 0, "bst-fj webhooks requires a Forgejo repository");
|
|
@@ -3999,48 +4005,241 @@ const webhooksCommand = defineCommand({
|
|
|
3999
4005
|
}
|
|
4000
4006
|
});
|
|
4001
4007
|
//#endregion
|
|
4002
|
-
//#region ../
|
|
4003
|
-
const INPUT_FLAG = "--input";
|
|
4004
|
-
const INPUT_FLAG_EQ = "--input=";
|
|
4008
|
+
//#region ../forgejo/src/actions.ts
|
|
4005
4009
|
/**
|
|
4006
|
-
*
|
|
4010
|
+
* The Actions reads: the one place in this package that talks to Forgejo's Actions surface, beside
|
|
4011
|
+
* `secrets.ts` which already speaks to `/actions/`. What used to be one composed read is now the
|
|
4012
|
+
* pieces that compose it -- {@link getActionsRunByNumber}, {@link listActionsRunJobs},
|
|
4013
|
+
* {@link fetchActionsJobLogById} -- with {@link fetchActionsJobLog} kept as the composed read its
|
|
4014
|
+
* existing callers use, and {@link listActionsRuns} beside them as the paged listing.
|
|
4007
4015
|
*
|
|
4008
|
-
*
|
|
4009
|
-
*
|
|
4010
|
-
*
|
|
4011
|
-
*
|
|
4012
|
-
*
|
|
4016
|
+
* **Checked against a live instance (2026-09-29).** A status entry's `targetUrl` carries
|
|
4017
|
+
* `{run, index}` and `run` there is the repo's
|
|
4018
|
+
* run **number**, not the run id the API's `/actions/runs/{id}` paths take -- passing the number
|
|
4019
|
+
* as an id silently reads an unrelated, older run. So the resolution is three reads: the run by
|
|
4020
|
+
* `?run_number=`, its jobs (a bare array, in the web UI's `/jobs/<index>` order), then the job's
|
|
4021
|
+
* log. It is **not** derived from `GET .../actions/tasks`, which was measured returning an
|
|
4022
|
+
* unrelated job's log for the same input. Every caller of {@link fetchActionsJobLog} must fail open on any error --
|
|
4023
|
+
* this module throws on one rather than swallowing it, exactly like every other read in this
|
|
4024
|
+
* package, so the caller's fail-open decision is made once, in one place, rather than half-hidden
|
|
4025
|
+
* in here.
|
|
4026
|
+
*/
|
|
4027
|
+
/**
|
|
4028
|
+
* A forge timestamp as the instance wrote it, or `null`. Nullish and empty are `null`, and so is
|
|
4029
|
+
* any time at or before the Unix epoch: the instance writes the epoch (in its own zone, such as
|
|
4030
|
+
* `1969-12-31T18:00:00-06:00`) for a run that has not started, and Go's zero time (`0001-...`)
|
|
4031
|
+
* elsewhere. Passing either through would make every duration derived from it meaningless.
|
|
4032
|
+
*/
|
|
4033
|
+
function forgeTimestamp(value) {
|
|
4034
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
4035
|
+
const ms = Date.parse(value);
|
|
4036
|
+
if (!Number.isFinite(ms) || ms <= 0) return null;
|
|
4037
|
+
return value;
|
|
4038
|
+
}
|
|
4039
|
+
const ActionsRunRecordSchema = z.object({
|
|
4040
|
+
id: z.number(),
|
|
4041
|
+
index_in_repo: z.number().nullish(),
|
|
4042
|
+
workflow_id: z.string().nullish(),
|
|
4043
|
+
title: z.string().nullish(),
|
|
4044
|
+
prettyref: z.string().nullish(),
|
|
4045
|
+
commit_sha: z.string().nullish(),
|
|
4046
|
+
event: z.string().nullish(),
|
|
4047
|
+
status: z.string().nullish(),
|
|
4048
|
+
started: z.string().nullish(),
|
|
4049
|
+
stopped: z.string().nullish(),
|
|
4050
|
+
/** Nanoseconds (a Go `time.Duration`); absent until the run finishes. */
|
|
4051
|
+
duration: z.number().nullish(),
|
|
4052
|
+
html_url: z.string().nullish()
|
|
4053
|
+
});
|
|
4054
|
+
const RunLookupResponseSchema = z.object({ workflow_runs: z.array(ActionsRunRecordSchema) });
|
|
4055
|
+
const ActionsJobRecordSchema = z.object({
|
|
4056
|
+
id: z.number(),
|
|
4057
|
+
name: z.string(),
|
|
4058
|
+
status: z.string()
|
|
4059
|
+
});
|
|
4060
|
+
const RunJobsResponseSchema = z.array(ActionsJobRecordSchema);
|
|
4061
|
+
function toActionsRun(record) {
|
|
4062
|
+
const duration = record.duration ?? null;
|
|
4063
|
+
return {
|
|
4064
|
+
id: record.id,
|
|
4065
|
+
number: record.index_in_repo ?? null,
|
|
4066
|
+
workflow: record.workflow_id ?? null,
|
|
4067
|
+
title: record.title ?? null,
|
|
4068
|
+
ref: record.prettyref ?? null,
|
|
4069
|
+
headSha: record.commit_sha ?? null,
|
|
4070
|
+
event: record.event ?? null,
|
|
4071
|
+
status: record.status ?? null,
|
|
4072
|
+
startedAt: forgeTimestamp(record.started),
|
|
4073
|
+
completedAt: forgeTimestamp(record.stopped),
|
|
4074
|
+
durationMs: duration === null || duration <= 0 ? null : Math.round(duration / 1e6),
|
|
4075
|
+
url: record.html_url ?? null
|
|
4076
|
+
};
|
|
4077
|
+
}
|
|
4078
|
+
/** A ceiling for a reader of the whole log, passed as `{ head: MAX_WHOLE_LOG_CHARS }`: well over
|
|
4079
|
+
* the 5 MB logs measured on this repo, and low enough that no reader receives an unbounded body. */
|
|
4080
|
+
const MAX_WHOLE_LOG_CHARS = 67108864;
|
|
4081
|
+
/**
|
|
4082
|
+
* Look a run up by the number the web UI and every `targetUrl` use, and report the whole record
|
|
4083
|
+
* rather than only its id. Sends `page` and `limit` with the filter -- without them the instance
|
|
4084
|
+
* answers a `run_number` query with every run it holds.
|
|
4085
|
+
*
|
|
4086
|
+
* No run with that number is a {@link FatalError}: the caller named something that does not
|
|
4087
|
+
* exist. A record whose number is not the one asked for is an {@link UnexpectedError}, because
|
|
4088
|
+
* an instance that ignored the filter would otherwise hand back its newest run under the
|
|
4089
|
+
* caller's number. Also throws on a non-2xx or an unparseable response.
|
|
4090
|
+
*/
|
|
4091
|
+
async function getActionsRunByNumber(executor, conn, runNumber) {
|
|
4092
|
+
const params = new URLSearchParams({
|
|
4093
|
+
run_number: runNumber,
|
|
4094
|
+
page: "1",
|
|
4095
|
+
limit: "1"
|
|
4096
|
+
});
|
|
4097
|
+
const url = `${conn.serverUrl}/api/v1/repos/${conn.repository}/actions/runs?${params.toString()}`;
|
|
4098
|
+
const res = await executor.fetch(url, { headers: { Authorization: `token ${conn.token}` } });
|
|
4099
|
+
if (!res.ok) await throwForgejoFailure(res, {
|
|
4100
|
+
verb: "look up run",
|
|
4101
|
+
resource: runNumber
|
|
4102
|
+
});
|
|
4103
|
+
const parsed = RunLookupResponseSchema.safeParse(await res.json());
|
|
4104
|
+
if (!parsed.success) throw new UnexpectedError(`Unexpected run lookup response for run ${runNumber}: ${parsed.error.message}`);
|
|
4105
|
+
const [run] = parsed.data.workflow_runs;
|
|
4106
|
+
if (run === void 0) throw new FatalError(`No run numbered ${runNumber} in ${conn.repository}`);
|
|
4107
|
+
const found = toActionsRun(run);
|
|
4108
|
+
if (found.number !== null && String(found.number) !== runNumber) throw new UnexpectedError(`Asked ${conn.repository} for run ${runNumber} and got run ${String(found.number)}: the forge ignored the run_number filter`);
|
|
4109
|
+
return found;
|
|
4110
|
+
}
|
|
4111
|
+
/**
|
|
4112
|
+
* A run's jobs, as a bare array, with `index` set to array position -- the same order the web UI's
|
|
4113
|
+
* `/jobs/<index>` links use. Throws on a non-2xx, an unparseable response, or a job record without
|
|
4114
|
+
* the three fields this module requires.
|
|
4013
4115
|
*/
|
|
4014
|
-
function
|
|
4015
|
-
const
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4116
|
+
async function listActionsRunJobs(executor, conn, runId) {
|
|
4117
|
+
const url = `${conn.serverUrl}/api/v1/repos/${conn.repository}/actions/runs/${String(runId)}/jobs`;
|
|
4118
|
+
const res = await executor.fetch(url, { headers: { Authorization: `token ${conn.token}` } });
|
|
4119
|
+
if (!res.ok) await throwForgejoFailure(res, {
|
|
4120
|
+
verb: "list run jobs",
|
|
4121
|
+
resource: String(runId)
|
|
4122
|
+
});
|
|
4123
|
+
const parsed = RunJobsResponseSchema.safeParse(await res.json());
|
|
4124
|
+
if (!parsed.success) throw new UnexpectedError(`Unexpected run-jobs response for run ${String(runId)}: ${parsed.error.message}`);
|
|
4125
|
+
const jobs = [];
|
|
4126
|
+
for (const [index, job] of parsed.data.entries()) jobs.push({
|
|
4127
|
+
index,
|
|
4128
|
+
id: job.id,
|
|
4129
|
+
name: job.name,
|
|
4130
|
+
status: job.status
|
|
4131
|
+
});
|
|
4132
|
+
return jobs;
|
|
4027
4133
|
}
|
|
4028
4134
|
/**
|
|
4029
|
-
*
|
|
4030
|
-
*
|
|
4031
|
-
*
|
|
4135
|
+
* Read one job's log by the numeric id the log endpoint wants, bounded by `bound`. Never throws on
|
|
4136
|
+
* a long log: the bound is what keeps a 68 MB-class response from reaching a caller, and
|
|
4137
|
+
* {@link ActionsJobLogText.truncated} says when it cut something. Throws on a non-2xx.
|
|
4032
4138
|
*/
|
|
4033
|
-
function
|
|
4034
|
-
const
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4139
|
+
async function fetchActionsJobLogById(executor, conn, jobId, bound) {
|
|
4140
|
+
const url = `${conn.serverUrl}/api/v1/repos/${conn.repository}/actions/jobs/${String(jobId)}/logs`;
|
|
4141
|
+
const res = await executor.fetch(url, { headers: { Authorization: `token ${conn.token}` } });
|
|
4142
|
+
if (!res.ok) await throwForgejoFailure(res, {
|
|
4143
|
+
verb: "read job log",
|
|
4144
|
+
resource: String(jobId)
|
|
4145
|
+
});
|
|
4146
|
+
const text = await res.text();
|
|
4147
|
+
if ("head" in bound) return {
|
|
4148
|
+
text: text.slice(0, bound.head),
|
|
4149
|
+
truncated: text.length > bound.head
|
|
4150
|
+
};
|
|
4151
|
+
const tail = bound.tail;
|
|
4152
|
+
if (text.length <= tail) return {
|
|
4153
|
+
text,
|
|
4154
|
+
truncated: false
|
|
4155
|
+
};
|
|
4156
|
+
return {
|
|
4157
|
+
text: text.slice(text.length - tail),
|
|
4158
|
+
truncated: true
|
|
4159
|
+
};
|
|
4160
|
+
}
|
|
4161
|
+
const RunsListResponseSchema = z.object({ workflow_runs: z.array(ActionsRunRecordSchema) });
|
|
4162
|
+
/**
|
|
4163
|
+
* The repository's workflow runs, newest first, paged **always** (spec decision 9). Every request
|
|
4164
|
+
* carries `page` and `limit`: measured against a live instance (2026-09-30), a runs query without
|
|
4165
|
+
* `page` ignores `limit` and answers with the whole table -- 3443 runs, 68 MB -- so paging is
|
|
4166
|
+
* structural here rather than a caller's obligation.
|
|
4167
|
+
*
|
|
4168
|
+
* The walk stops on an empty page or once `limit` rows are in hand. A page **shorter** than asked
|
|
4169
|
+
* for does not stop it: the server may clamp `limit` to its own page size, and reading a short page
|
|
4170
|
+
* as the end of the table would silently answer about part of it. Hitting the page cap with rows
|
|
4171
|
+
* still arriving warns, because a truncated listing otherwise reads as absence.
|
|
4172
|
+
*/
|
|
4173
|
+
async function listActionsRuns(executor, conn, options) {
|
|
4174
|
+
const headers = { Authorization: `token ${conn.token}` };
|
|
4175
|
+
const pageSize = Math.min(options.limit, 50);
|
|
4176
|
+
const runs = [];
|
|
4177
|
+
for (let page = 1; page <= 20; page++) {
|
|
4178
|
+
const params = new URLSearchParams({
|
|
4179
|
+
page: String(page),
|
|
4180
|
+
limit: String(pageSize)
|
|
4181
|
+
});
|
|
4182
|
+
if (options.workflow !== void 0) params.set("workflow_id", options.workflow);
|
|
4183
|
+
if (options.ref !== void 0) params.set("ref", options.ref);
|
|
4184
|
+
if (options.headSha !== void 0) params.set("head_sha", options.headSha);
|
|
4185
|
+
if (options.status !== void 0) params.set("status", options.status);
|
|
4186
|
+
const url = `${conn.serverUrl}/api/v1/repos/${conn.repository}/actions/runs?${params.toString()}`;
|
|
4187
|
+
const res = await executor.fetch(url, { headers });
|
|
4188
|
+
if (!res.ok) await throwForgejoFailure(res, {
|
|
4189
|
+
verb: "list workflow runs",
|
|
4190
|
+
resource: conn.repository
|
|
4191
|
+
});
|
|
4192
|
+
const parsed = RunsListResponseSchema.safeParse(await res.json());
|
|
4193
|
+
if (!parsed.success) throw new UnexpectedError(`Unexpected workflow-runs response for ${conn.repository}: ${parsed.error.message}`);
|
|
4194
|
+
const pageRuns = parsed.data.workflow_runs.map(toActionsRun);
|
|
4195
|
+
runs.push(...pageRuns);
|
|
4196
|
+
if (pageRuns.length === 0) break;
|
|
4197
|
+
if (runs.length >= options.limit) break;
|
|
4198
|
+
if (page === 20) {
|
|
4199
|
+
log.warn(`Workflow run listing on ${conn.repository} hit the ${String(20)}-page cap without an empty page; results may be truncated`);
|
|
4200
|
+
options.onPageCap?.();
|
|
4201
|
+
}
|
|
4039
4202
|
}
|
|
4040
|
-
return
|
|
4203
|
+
return runs.slice(0, options.limit);
|
|
4204
|
+
}
|
|
4205
|
+
/** A `targetUrl` path of the shape `/actions/runs/{run}/jobs/{index}`, tolerating a scheme and
|
|
4206
|
+
* host in front of it and a trailing path segment, query, or fragment after it. */
|
|
4207
|
+
const ACTIONS_JOB_TARGET_URL = /\/actions\/runs\/(\d+)\/jobs\/(\d+)(?:[/?#]|$)/u;
|
|
4208
|
+
/**
|
|
4209
|
+
* Parse a status entry's `targetUrl` into the `{run, index}` the job-log read needs. A `targetUrl`
|
|
4210
|
+
* of any other shape -- including `null` -- yields `null`, full stop: **the id is never derived
|
|
4211
|
+
* from the `/actions/tasks` listing**, which was measured (against a live instance, in an earlier
|
|
4212
|
+
* investigation) returning an unrelated job's log for the same input. This module's job-log read
|
|
4213
|
+
* accepts nothing but this parser's output for exactly that reason.
|
|
4214
|
+
*
|
|
4215
|
+
* The parser lives here rather than beside the incident sweep that first needed it (spec decision
|
|
4216
|
+
* 15) because a caller holding a pasted `targetUrl` must be able to read the same object, the URL
|
|
4217
|
+
* shape is this package's domain, and a second copy in a command-line tool would be a forked
|
|
4218
|
+
* declaration.
|
|
4219
|
+
*/
|
|
4220
|
+
function parseActionsJobTargetUrl(targetUrl) {
|
|
4221
|
+
if (targetUrl === null) return null;
|
|
4222
|
+
const match = ACTIONS_JOB_TARGET_URL.exec(targetUrl);
|
|
4223
|
+
if (match === null) return null;
|
|
4224
|
+
return {
|
|
4225
|
+
run: match[1],
|
|
4226
|
+
index: Number(match[2])
|
|
4227
|
+
};
|
|
4228
|
+
}
|
|
4229
|
+
/**
|
|
4230
|
+
* The repository half of the same URL: everything before the first `/actions/runs/` is a
|
|
4231
|
+
* repository URL when it carries a scheme, so a pasted `targetUrl` can name the repository it came
|
|
4232
|
+
* from instead of leaving that to a separate option. A bare path carries no repository and yields
|
|
4233
|
+
* `undefined`; a prefixed value that is not a repository URL is refused by
|
|
4234
|
+
* {@link parseRepoTarget}, which is the answer a caller who pasted the wrong thing needs.
|
|
4235
|
+
*/
|
|
4236
|
+
function parseActionsJobTargetRepo(targetUrl) {
|
|
4237
|
+
const boundary = targetUrl.indexOf("/actions/runs/");
|
|
4238
|
+
if (boundary === -1) return void 0;
|
|
4239
|
+
const prefix = targetUrl.slice(0, boundary);
|
|
4240
|
+
if (!prefix.startsWith("http://") && !prefix.startsWith("https://")) return void 0;
|
|
4241
|
+
return parseRepoTarget(prefix);
|
|
4041
4242
|
}
|
|
4042
|
-
z.object({ workflow_runs: z.array(z.object({ id: z.number() })) });
|
|
4043
|
-
z.array(z.object({ id: z.number() }));
|
|
4044
4243
|
/**
|
|
4045
4244
|
* Trigger a `workflow_dispatch` event. Any 2xx is success (the instance's swagger lists 201 and
|
|
4046
4245
|
* 204). A failure is a {@link FatalError} carrying {@link forgejoErrorMessage}'s text for every
|
|
@@ -4070,79 +4269,551 @@ async function dispatchWorkflow(executor, conn, options) {
|
|
|
4070
4269
|
};
|
|
4071
4270
|
}
|
|
4072
4271
|
//#endregion
|
|
4073
|
-
//#region src/
|
|
4272
|
+
//#region ../forgejo/src/actions-log.ts
|
|
4273
|
+
/** A CSI sequence: `ESC [` , parameter bytes, intermediate bytes, one final byte. Removed whole --
|
|
4274
|
+
* dropping only the escape byte would leave the parameters behind as text. */
|
|
4275
|
+
const CSI_SEQUENCE = /\u001b\[[0-?]*[ -/]*[@-~]/gu;
|
|
4276
|
+
/** An OSC sequence: `ESC ]` up to a BEL or a string terminator (`ESC \`). */
|
|
4277
|
+
const OSC_SEQUENCE = /\u001b\][^\u0007]*(?:\u0007|\u001b\\)/gu;
|
|
4278
|
+
/** An escape byte that began neither of those. */
|
|
4279
|
+
const LONE_ESCAPE = /\u001b/gu;
|
|
4280
|
+
/** The C0 controls that may still be left, tab excepted, plus DEL. */
|
|
4281
|
+
const CONTROL_CHARS = /[\u0000-\u0008\u000a-\u001f\u007f]/gu;
|
|
4282
|
+
/** The first token of a line, when it is shaped like the timestamp the runner writes. */
|
|
4283
|
+
const LINE_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\S+$/u;
|
|
4284
|
+
/** Remove every control sequence and control character, whole. */
|
|
4285
|
+
function stripControlSequences(text) {
|
|
4286
|
+
return text.replace(CSI_SEQUENCE, "").replace(OSC_SEQUENCE, "").replace(LONE_ESCAPE, "").replace(CONTROL_CHARS, "");
|
|
4287
|
+
}
|
|
4288
|
+
/**
|
|
4289
|
+
* Split one line into its timestamp and its text. The first whitespace-delimited token is the
|
|
4290
|
+
* timestamp only when it is shaped like one **and** a clock accepts it -- a job printing a
|
|
4291
|
+
* date-shaped word keeps it in its body rather than losing it to a `null`.
|
|
4292
|
+
*/
|
|
4293
|
+
function splitLogLine(line) {
|
|
4294
|
+
const spaceAt = line.indexOf(" ");
|
|
4295
|
+
const head = spaceAt === -1 ? line : line.slice(0, spaceAt);
|
|
4296
|
+
if (LINE_TIMESTAMP.test(head) && Number.isFinite(Date.parse(head))) return {
|
|
4297
|
+
at: head,
|
|
4298
|
+
text: stripControlSequences(spaceAt === -1 ? "" : line.slice(spaceAt + 1))
|
|
4299
|
+
};
|
|
4300
|
+
return {
|
|
4301
|
+
at: null,
|
|
4302
|
+
text: stripControlSequences(line)
|
|
4303
|
+
};
|
|
4304
|
+
}
|
|
4305
|
+
function spanBetween(start, end) {
|
|
4306
|
+
if (start === null || end === null) return null;
|
|
4307
|
+
const startMs = Date.parse(start);
|
|
4308
|
+
const endMs = Date.parse(end);
|
|
4309
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) return null;
|
|
4310
|
+
return endMs - startMs;
|
|
4311
|
+
}
|
|
4074
4312
|
/**
|
|
4075
|
-
*
|
|
4076
|
-
*
|
|
4313
|
+
* Read a job log as lines. The carriage return of a CRLF pair and the empty line a trailing
|
|
4314
|
+
* newline leaves are both dropped.
|
|
4077
4315
|
*/
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
});
|
|
4316
|
+
function readActionsJobLog(text) {
|
|
4317
|
+
const raw = text.split("\n");
|
|
4318
|
+
if (raw.length > 0 && raw[raw.length - 1] === "") raw.pop();
|
|
4319
|
+
const lines = raw.map((entry) => splitLogLine(entry.endsWith("\r") ? entry.slice(0, -1) : entry));
|
|
4320
|
+
const firstLineAt = lines.find((line) => line.at !== null)?.at ?? null;
|
|
4321
|
+
let lastLineAt = null;
|
|
4322
|
+
for (let i = lines.length - 1; i >= 0 && lastLineAt === null; i--) lastLineAt = lines[i]?.at ?? null;
|
|
4086
4323
|
return {
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
ref: dispatched.ref,
|
|
4092
|
-
inputs: dispatched.inputs
|
|
4324
|
+
firstLineAt,
|
|
4325
|
+
lastLineAt,
|
|
4326
|
+
spanMs: spanBetween(firstLineAt, lastLineAt),
|
|
4327
|
+
lines
|
|
4093
4328
|
};
|
|
4094
4329
|
}
|
|
4095
4330
|
//#endregion
|
|
4096
|
-
//#region src/
|
|
4097
|
-
|
|
4331
|
+
//#region src/ci-log.ts
|
|
4332
|
+
/**
|
|
4333
|
+
* `bst-fj ci:log` -- one CI job's log, whole, or the lines of it a `--grep` matches. GitHub has
|
|
4334
|
+
* `gh run view --log` for this, so the command is Forgejo-only, like every other command in this
|
|
4335
|
+
* bin.
|
|
4336
|
+
*
|
|
4337
|
+
* The positional is a run number, a run URL (what `ci:runs` prints), or a status `target_url` (what
|
|
4338
|
+
* `pr:checks` and `commit:status` print beside a red check), so a caller pastes rather than
|
|
4339
|
+
* transcribes.
|
|
4340
|
+
*
|
|
4341
|
+
* The log is read **whole** up to the library's ceiling, not as the 2 MiB tail the incident body
|
|
4342
|
+
* keeps: a `--grep` over a tail would answer "not mentioned" about lines it never saw. A log cut
|
|
4343
|
+
* by the ceiling says so in `log.truncated` and in a notice.
|
|
4344
|
+
*
|
|
4345
|
+
* There is no step table. The runner on the instance this was built against writes a step marker
|
|
4346
|
+
* for its first main step only, so a step split read off the log text was wrong for every later
|
|
4347
|
+
* step (see `@bensandee/forgejo/actions-log`).
|
|
4348
|
+
*
|
|
4349
|
+
* Under `--json` the payload carries `lines` only with `--grep`: a whole log is megabytes, and a
|
|
4350
|
+
* reader that wants it all reads the text output.
|
|
4351
|
+
*/
|
|
4352
|
+
/** A run page URL: `/actions/runs/{run}`, with nothing after it but a slash, query or fragment. */
|
|
4353
|
+
const ACTIONS_RUN_URL = /\/actions\/runs\/(\d+)\/?(?:[?#]|$)/u;
|
|
4354
|
+
/**
|
|
4355
|
+
* The positional: all digits is a run number; a job URL gives the run and the job index; a run
|
|
4356
|
+
* URL gives the run alone. A URL carrying a host and `owner/repo` also names the repository,
|
|
4357
|
+
* which then stands in for `--repo`.
|
|
4358
|
+
*/
|
|
4359
|
+
function parseCiLogTarget(target) {
|
|
4360
|
+
if (/^\d+$/.test(target)) return {
|
|
4361
|
+
run: target,
|
|
4362
|
+
index: null,
|
|
4363
|
+
repoTarget: void 0
|
|
4364
|
+
};
|
|
4365
|
+
const location = parseActionsJobTargetUrl(target);
|
|
4366
|
+
if (location !== null) return {
|
|
4367
|
+
run: location.run,
|
|
4368
|
+
index: location.index,
|
|
4369
|
+
repoTarget: parseActionsJobTargetRepo(target)
|
|
4370
|
+
};
|
|
4371
|
+
const runOnly = ACTIONS_RUN_URL.exec(target);
|
|
4372
|
+
if (runOnly !== null) return {
|
|
4373
|
+
run: runOnly[1],
|
|
4374
|
+
index: null,
|
|
4375
|
+
repoTarget: parseActionsJobTargetRepo(target)
|
|
4376
|
+
};
|
|
4377
|
+
throw new FatalError(`Not a run number or an Actions run or job URL: "${target}" (expected e.g. 6693, https://host/owner/repo/actions/runs/6693 or .../actions/runs/6693/jobs/0).`);
|
|
4378
|
+
}
|
|
4379
|
+
/** What the run's jobs are, in the shape an error message lists them in for a caller to read. */
|
|
4380
|
+
function jobTable(jobs) {
|
|
4381
|
+
return jobs.map((candidate) => ` ${String(candidate.index)} ${candidate.name} ${candidate.status}`).join("\n");
|
|
4382
|
+
}
|
|
4383
|
+
function jobAtIndex(jobs, index, label) {
|
|
4384
|
+
const found = jobs.find((candidate) => candidate.index === index);
|
|
4385
|
+
if (found === void 0) throw new FatalError(`${label} ${String(index)} is not one of this run's jobs. Jobs in this run:\n${jobTable(jobs)}`);
|
|
4386
|
+
return found;
|
|
4387
|
+
}
|
|
4388
|
+
/**
|
|
4389
|
+
* Which job's log to read.
|
|
4390
|
+
*
|
|
4391
|
+
* A `--job` of all digits is an index -- the same index a `target_url` carries -- and anything else
|
|
4392
|
+
* is an exact, case-insensitive job name. When the URL already named an index, a `--job` that
|
|
4393
|
+
* disagrees with it is a refusal naming both, because either could be what the caller meant.
|
|
4394
|
+
*
|
|
4395
|
+
* With neither and several jobs, this refuses rather than defaulting to index 0: the answer would
|
|
4396
|
+
* be about the wrong job, confidently, with nothing said.
|
|
4397
|
+
*/
|
|
4398
|
+
function chooseJob(jobs, job, urlIndex) {
|
|
4399
|
+
if (job !== void 0) {
|
|
4400
|
+
const chosen = /^\d+$/.test(job) ? jobAtIndex(jobs, Number(job), `--job ${job}`) : jobs.find((candidate) => candidate.name.toLowerCase() === job.toLowerCase());
|
|
4401
|
+
if (chosen === void 0) throw new FatalError(`No job is named "${job}". Jobs in this run:\n${jobTable(jobs)}`);
|
|
4402
|
+
if (urlIndex !== null && chosen.index !== urlIndex) throw new FatalError(`--job "${job}" is job ${String(chosen.index)}, but the URL names job ${String(urlIndex)} -- give one of them.`);
|
|
4403
|
+
return chosen;
|
|
4404
|
+
}
|
|
4405
|
+
if (urlIndex !== null) return jobAtIndex(jobs, urlIndex, "The URL's job index");
|
|
4406
|
+
const only = jobs.length === 1 ? jobs[0] : void 0;
|
|
4407
|
+
if (only !== void 0) return only;
|
|
4408
|
+
throw new FatalError(`This run has ${String(jobs.length)} jobs, so --job must say which one (a name or the index).\nJobs in this run:\n${jobTable(jobs)}`);
|
|
4409
|
+
}
|
|
4410
|
+
/**
|
|
4411
|
+
* Read one job's log: run by number, its jobs, the chosen job, the whole log. A refused target, a
|
|
4412
|
+
* missing run or job, and a non-2xx from the forge all throw; a successful read of a failed job
|
|
4413
|
+
* does not, because this is a readout and not a verdict.
|
|
4414
|
+
*/
|
|
4415
|
+
async function runCiLog(fetcher, options) {
|
|
4416
|
+
const target = parseCiLogTarget(options.target);
|
|
4417
|
+
let grep;
|
|
4418
|
+
if (options.grep !== void 0) try {
|
|
4419
|
+
grep = new RegExp(options.grep);
|
|
4420
|
+
} catch {
|
|
4421
|
+
throw new FatalError(`--grep is not a valid JavaScript regular expression: "${options.grep}"`);
|
|
4422
|
+
}
|
|
4423
|
+
const conn = requireForgejoTarget(options.cwd, resolveIssueTarget(target.repoTarget, options.repo), "ci:log is Forgejo-only -- gh run view --log already serves GitHub, first-class. Use that instead.");
|
|
4424
|
+
rootLogger.debug(`ci:log repository=${conn.repository} run=${target.run}`);
|
|
4425
|
+
const run = await getActionsRunByNumber(fetcher, conn, target.run);
|
|
4426
|
+
const chosen = chooseJob(await listActionsRunJobs(fetcher, conn, run.id), options.job, target.index);
|
|
4427
|
+
const jobLog = await fetchActionsJobLogById(fetcher, conn, chosen.id, { head: MAX_WHOLE_LOG_CHARS });
|
|
4428
|
+
const read = readActionsJobLog(jobLog.text);
|
|
4429
|
+
const notices = [];
|
|
4430
|
+
if (jobLog.truncated) notices.push("The job log is longer than the whole-log ceiling, so only its head was read: later lines are missing.");
|
|
4431
|
+
return {
|
|
4432
|
+
repository: conn.repository,
|
|
4433
|
+
run: {
|
|
4434
|
+
number: run.number ?? Number.parseInt(target.run, 10),
|
|
4435
|
+
id: run.id,
|
|
4436
|
+
workflow: run.workflow,
|
|
4437
|
+
ref: run.ref,
|
|
4438
|
+
headSha: run.headSha,
|
|
4439
|
+
status: run.status
|
|
4440
|
+
},
|
|
4441
|
+
job: {
|
|
4442
|
+
index: chosen.index,
|
|
4443
|
+
id: chosen.id,
|
|
4444
|
+
name: chosen.name,
|
|
4445
|
+
status: chosen.status
|
|
4446
|
+
},
|
|
4447
|
+
log: {
|
|
4448
|
+
firstLineAt: read.firstLineAt,
|
|
4449
|
+
lastLineAt: read.lastLineAt,
|
|
4450
|
+
spanMs: read.spanMs,
|
|
4451
|
+
lineCount: read.lines.length,
|
|
4452
|
+
truncated: jobLog.truncated
|
|
4453
|
+
},
|
|
4454
|
+
lines: grep === void 0 ? read.lines : read.lines.filter((line) => grep.test(line.text)),
|
|
4455
|
+
grep: grep !== void 0,
|
|
4456
|
+
notices
|
|
4457
|
+
};
|
|
4458
|
+
}
|
|
4459
|
+
/** A two-line header naming the run and job, then each line kept with its timestamp. */
|
|
4460
|
+
function renderCiLog(result) {
|
|
4461
|
+
const lines = [`run ${String(result.run.number)} ${result.run.status ?? "?"} ${result.run.workflow ?? "?"}`, `job ${String(result.job.index)} ${result.job.name} (${result.job.status})`];
|
|
4462
|
+
for (const line of result.lines) lines.push(line.at === null ? line.text : `${line.at} ${line.text}`);
|
|
4463
|
+
return lines.join("\n");
|
|
4464
|
+
}
|
|
4465
|
+
//#endregion
|
|
4466
|
+
//#region src/commands/ci-log.ts
|
|
4467
|
+
const ciLogCommand = defineCommand({
|
|
4098
4468
|
meta: {
|
|
4099
|
-
name: "
|
|
4100
|
-
description: "
|
|
4469
|
+
name: "ci:log",
|
|
4470
|
+
description: "Show one workflow job's log, or the lines matching --grep (Forgejo-only; use gh run view --log on GitHub)"
|
|
4101
4471
|
},
|
|
4102
4472
|
args: {
|
|
4473
|
+
target: {
|
|
4474
|
+
type: "positional",
|
|
4475
|
+
required: true,
|
|
4476
|
+
description: "Run number, run URL (actions/runs/<run>), or a status target_url (actions/runs/<run>/jobs/<index>)"
|
|
4477
|
+
},
|
|
4478
|
+
job: {
|
|
4479
|
+
type: "string",
|
|
4480
|
+
description: "Job name (case-insensitive) or zero-based index (refused when a URL names one)"
|
|
4481
|
+
},
|
|
4482
|
+
grep: {
|
|
4483
|
+
type: "string",
|
|
4484
|
+
description: "JavaScript regular expression over each line's text; prints only matching lines"
|
|
4485
|
+
},
|
|
4103
4486
|
repo: {
|
|
4104
4487
|
type: "string",
|
|
4105
|
-
description: "
|
|
4106
|
-
required: false
|
|
4488
|
+
description: "Repository URL (default: this repo)"
|
|
4107
4489
|
},
|
|
4490
|
+
json: {
|
|
4491
|
+
type: "boolean",
|
|
4492
|
+
description: "Emit one JSON document: {repository, run: {number, id, workflow, ref, headSha, status}, job: {index, id, name, status}, log: {firstLineAt, lastLineAt, spanMs, lineCount, truncated}, lines ([{at, text}], with --grep only)}."
|
|
4493
|
+
},
|
|
4494
|
+
verbose: {
|
|
4495
|
+
type: "boolean",
|
|
4496
|
+
description: "Emit detailed debug output"
|
|
4497
|
+
}
|
|
4498
|
+
},
|
|
4499
|
+
async run({ args }) {
|
|
4500
|
+
const out = createCliOutput(args.json === true);
|
|
4501
|
+
const target = args.target ?? "";
|
|
4502
|
+
const result = await runCiLog(cliFetcher(), {
|
|
4503
|
+
cwd: process.cwd(),
|
|
4504
|
+
target,
|
|
4505
|
+
...args.job === void 0 ? {} : { job: args.job },
|
|
4506
|
+
...args.grep === void 0 ? {} : { grep: args.grep },
|
|
4507
|
+
...args.repo === void 0 ? {} : { repo: args.repo }
|
|
4508
|
+
});
|
|
4509
|
+
for (const notice of result.notices) out.notice(notice);
|
|
4510
|
+
out.json({
|
|
4511
|
+
repository: result.repository,
|
|
4512
|
+
run: result.run,
|
|
4513
|
+
job: result.job,
|
|
4514
|
+
log: result.log,
|
|
4515
|
+
...result.grep ? { lines: result.lines } : {}
|
|
4516
|
+
});
|
|
4517
|
+
out.text(renderCiLog(result));
|
|
4518
|
+
}
|
|
4519
|
+
});
|
|
4520
|
+
//#endregion
|
|
4521
|
+
//#region src/ci-runs.ts
|
|
4522
|
+
/**
|
|
4523
|
+
* `bst-fj ci:runs` -- a read-only, Forgejo-only listing of a repository's workflow runs and their
|
|
4524
|
+
* jobs, over the shared paged read in `@bensandee/forgejo/actions`. GitHub run listing already has
|
|
4525
|
+
* a first-class tool (`gh run list`), the same precedent `pr:list` follows for the same structural
|
|
4526
|
+
* reason (spec decision 2 and 13's target form: `--repo` is a repository URL, like every `pr:*`
|
|
4527
|
+
* and `issue:*` read).
|
|
4528
|
+
*
|
|
4529
|
+
* `--ref` and `--pr` are one filter spelled two ways (spec decision 6): a pull-request run carries
|
|
4530
|
+
* `refs/pull/<n>/head`, which no caller should have to know, and the branch's own push runs stay
|
|
4531
|
+
* one `--ref <branch>` away.
|
|
4532
|
+
*/
|
|
4533
|
+
function parseLimit$2(raw) {
|
|
4534
|
+
const limit = Number(raw);
|
|
4535
|
+
if (!Number.isInteger(limit) || limit < 1) throw new FatalError(`Invalid --limit "${raw}" (expected a positive integer).`);
|
|
4536
|
+
return limit;
|
|
4537
|
+
}
|
|
4538
|
+
/**
|
|
4539
|
+
* The `ref` filter, from whichever of `--ref` and `--pr` was given. A bare `--ref main` names a
|
|
4540
|
+
* branch, so it becomes `refs/heads/main`; a value that is already a full ref is sent as is;
|
|
4541
|
+
* `--pr` becomes `refs/pull/<n>/head` (spec decision 6) and carries the repository off a PR URL so
|
|
4542
|
+
* the URL can stand in for `--repo`.
|
|
4543
|
+
*
|
|
4544
|
+
* Both together is a refusal rather than a precedence: they are the same filter, and quietly
|
|
4545
|
+
* ignoring one answers about a different set of runs than the caller asked for.
|
|
4546
|
+
*/
|
|
4547
|
+
function resolveRunsRef(ref, pr) {
|
|
4548
|
+
if (ref !== void 0 && pr !== void 0) throw new FatalError("--ref and --pr are the same filter -- give one of them (--pr 273 for a pull request's own runs, --ref main for a branch's).");
|
|
4549
|
+
if (pr !== void 0) {
|
|
4550
|
+
const parsed = parseIssueRef(pr);
|
|
4551
|
+
if (parsed.number === void 0) throw new FatalError(`Not a pull request number or URL: "${pr}"`);
|
|
4552
|
+
return {
|
|
4553
|
+
ref: `refs/pull/${String(parsed.number)}/head`,
|
|
4554
|
+
target: parsed.target
|
|
4555
|
+
};
|
|
4556
|
+
}
|
|
4557
|
+
if (ref === void 0) return {
|
|
4558
|
+
ref: void 0,
|
|
4559
|
+
target: void 0
|
|
4560
|
+
};
|
|
4561
|
+
if (ref.startsWith("refs/")) return {
|
|
4562
|
+
ref,
|
|
4563
|
+
target: void 0
|
|
4564
|
+
};
|
|
4565
|
+
return {
|
|
4566
|
+
ref: `refs/heads/${ref}`,
|
|
4567
|
+
target: void 0
|
|
4568
|
+
};
|
|
4569
|
+
}
|
|
4570
|
+
/**
|
|
4571
|
+
* List a repository's workflow runs, newest first, each with its jobs. Paging is never optional on
|
|
4572
|
+
* this read (spec decision 9): an unpaged runs query answers with the whole table. Refuses a
|
|
4573
|
+
* github.com target before any fetch.
|
|
4574
|
+
*/
|
|
4575
|
+
async function runCiRuns(fetcher, options) {
|
|
4576
|
+
const limit = parseLimit$2(options.limit);
|
|
4577
|
+
const resolved = resolveRunsRef(options.ref, options.pr);
|
|
4578
|
+
const conn = requireForgejoTarget(options.cwd, resolveIssueTarget(resolved.target, options.repo), "ci:runs is Forgejo-only -- gh run list already serves GitHub, first-class. Use that instead.");
|
|
4579
|
+
rootLogger.debug(`ci:runs repository=${conn.repository} limit=${String(limit)}`);
|
|
4580
|
+
const notices = [];
|
|
4581
|
+
const runs = await listActionsRuns(fetcher, conn, {
|
|
4582
|
+
limit,
|
|
4583
|
+
workflow: options.workflow,
|
|
4584
|
+
ref: resolved.ref,
|
|
4585
|
+
headSha: options.sha,
|
|
4586
|
+
status: options.status,
|
|
4587
|
+
onPageCap: () => {
|
|
4588
|
+
notices.push("Workflow run listing hit the page cap before an empty page; results may be truncated.");
|
|
4589
|
+
}
|
|
4590
|
+
});
|
|
4591
|
+
const fetched = await Promise.all(runs.map(async (run) => ({
|
|
4592
|
+
run,
|
|
4593
|
+
jobs: await listActionsRunJobs(fetcher, conn, run.id)
|
|
4594
|
+
})));
|
|
4595
|
+
const rows = [];
|
|
4596
|
+
for (const { run, jobs } of fetched) rows.push({
|
|
4597
|
+
...run,
|
|
4598
|
+
jobs
|
|
4599
|
+
});
|
|
4600
|
+
return {
|
|
4601
|
+
repository: conn.repository,
|
|
4602
|
+
runs: rows,
|
|
4603
|
+
notices
|
|
4604
|
+
};
|
|
4605
|
+
}
|
|
4606
|
+
/** A duration, or the dash that says the forge reported none (the run has not finished). */
|
|
4607
|
+
function durationText(durationMs) {
|
|
4608
|
+
return durationMs === null ? "-" : `${String(durationMs)}ms`;
|
|
4609
|
+
}
|
|
4610
|
+
/**
|
|
4611
|
+
* One line per run, then one indented line per job. The run number leads its line because it is
|
|
4612
|
+
* what `ci:log` takes as its positional.
|
|
4613
|
+
*/
|
|
4614
|
+
function renderCiRuns(result) {
|
|
4615
|
+
if (result.runs.length === 0) return `No workflow runs in ${result.repository}.`;
|
|
4616
|
+
const lines = [];
|
|
4617
|
+
for (const run of result.runs) {
|
|
4618
|
+
lines.push([
|
|
4619
|
+
run.number === null ? "?" : String(run.number),
|
|
4620
|
+
run.status ?? "?",
|
|
4621
|
+
run.workflow ?? "?",
|
|
4622
|
+
run.event ?? "?",
|
|
4623
|
+
run.ref ?? "?",
|
|
4624
|
+
run.headSha ?? "?",
|
|
4625
|
+
durationText(run.durationMs),
|
|
4626
|
+
run.url ?? "?"
|
|
4627
|
+
].join(" "));
|
|
4628
|
+
for (const job of run.jobs) lines.push(` ${String(job.index)} ${job.status} ${job.name}`);
|
|
4629
|
+
}
|
|
4630
|
+
return lines.join("\n");
|
|
4631
|
+
}
|
|
4632
|
+
//#endregion
|
|
4633
|
+
//#region src/commands/ci-runs.ts
|
|
4634
|
+
const ciRunsCommand = defineCommand({
|
|
4635
|
+
meta: {
|
|
4636
|
+
name: "ci:runs",
|
|
4637
|
+
description: "List a repository's workflow runs with their jobs, newest first (Forgejo-only; use gh run list on GitHub)"
|
|
4638
|
+
},
|
|
4639
|
+
args: {
|
|
4108
4640
|
workflow: {
|
|
4109
4641
|
type: "string",
|
|
4110
|
-
description: "Workflow
|
|
4111
|
-
required: true
|
|
4642
|
+
description: "Workflow file name, e.g. check.yml (the forge's workflow_id)"
|
|
4112
4643
|
},
|
|
4113
4644
|
ref: {
|
|
4114
4645
|
type: "string",
|
|
4115
|
-
description: "
|
|
4116
|
-
required: false
|
|
4646
|
+
description: "Branch or full ref; a bare name means refs/heads/<name> (refuses --pr)"
|
|
4117
4647
|
},
|
|
4118
|
-
|
|
4648
|
+
pr: {
|
|
4119
4649
|
type: "string",
|
|
4120
|
-
description: "
|
|
4121
|
-
|
|
4650
|
+
description: "Pull request number or URL: its own runs (refs/pull/<n>/head)"
|
|
4651
|
+
},
|
|
4652
|
+
sha: {
|
|
4653
|
+
type: "string",
|
|
4654
|
+
description: "Head SHA (the forge's head_sha)"
|
|
4655
|
+
},
|
|
4656
|
+
status: {
|
|
4657
|
+
type: "string",
|
|
4658
|
+
description: "Run status: pending | queued | in_progress | failure | success | skipped (the forge interprets the string)"
|
|
4659
|
+
},
|
|
4660
|
+
limit: {
|
|
4661
|
+
type: "string",
|
|
4662
|
+
default: "20",
|
|
4663
|
+
description: "Maximum runs to return"
|
|
4664
|
+
},
|
|
4665
|
+
repo: {
|
|
4666
|
+
type: "string",
|
|
4667
|
+
description: "Repository URL (default: this repo)"
|
|
4122
4668
|
},
|
|
4123
4669
|
json: {
|
|
4124
4670
|
type: "boolean",
|
|
4125
|
-
description: "Emit
|
|
4671
|
+
description: "Emit one JSON document: {repository, runs: [{number, id, workflow, title, ref (main, or #<pr> for a PR run), headSha, event, status, startedAt, completedAt, durationMs, url (each nullable but id), jobs: [{index, id, name, status}]}]}."
|
|
4126
4672
|
},
|
|
4127
4673
|
verbose: {
|
|
4128
4674
|
type: "boolean",
|
|
4129
4675
|
description: "Emit detailed debug output"
|
|
4130
4676
|
}
|
|
4131
4677
|
},
|
|
4132
|
-
async run({ args
|
|
4678
|
+
async run({ args }) {
|
|
4133
4679
|
const out = createCliOutput(args.json === true);
|
|
4134
|
-
|
|
4135
|
-
const result = await runWorkflowDispatch(cliFetcher(), {
|
|
4680
|
+
const result = await runCiRuns(cliFetcher(), {
|
|
4136
4681
|
cwd: process.cwd(),
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4682
|
+
workflow: args.workflow ?? void 0,
|
|
4683
|
+
ref: args.ref ?? void 0,
|
|
4684
|
+
pr: args.pr ?? void 0,
|
|
4685
|
+
sha: args.sha ?? void 0,
|
|
4686
|
+
status: args.status ?? void 0,
|
|
4687
|
+
limit: args.limit,
|
|
4688
|
+
repo: args.repo ?? void 0
|
|
4141
4689
|
});
|
|
4142
|
-
out.
|
|
4143
|
-
out.
|
|
4690
|
+
for (const notice of result.notices) out.notice(notice);
|
|
4691
|
+
out.json({
|
|
4692
|
+
repository: result.repository,
|
|
4693
|
+
runs: result.runs
|
|
4694
|
+
});
|
|
4695
|
+
out.text(renderCiRuns(result));
|
|
4144
4696
|
}
|
|
4145
|
-
})
|
|
4697
|
+
});
|
|
4698
|
+
//#endregion
|
|
4699
|
+
//#region ../cli-kit/src/workflow-inputs.ts
|
|
4700
|
+
const INPUT_FLAG = "--input";
|
|
4701
|
+
const INPUT_FLAG_EQ = "--input=";
|
|
4702
|
+
/**
|
|
4703
|
+
* Collect every `--input` value from a command's raw CLI args.
|
|
4704
|
+
*
|
|
4705
|
+
* citty@0.2.2 keeps only the LAST occurrence of a repeated named flag, so a
|
|
4706
|
+
* declared `input` arg cannot capture `--input a=1 --input b=2` -- it silently
|
|
4707
|
+
* yields just `b=2`. Scanning the raw argv directly recovers all occurrences.
|
|
4708
|
+
* Both `--input k=v` (two tokens) and `--input=k=v` (one token) forms are
|
|
4709
|
+
* supported. Throws {@link FatalError} when `--input` has no following value.
|
|
4710
|
+
*/
|
|
4711
|
+
function collectInputArgs(rawArgs) {
|
|
4712
|
+
const values = [];
|
|
4713
|
+
for (let i = 0; i < rawArgs.length; i += 1) {
|
|
4714
|
+
const arg = rawArgs[i];
|
|
4715
|
+
if (arg === void 0) continue;
|
|
4716
|
+
if (arg === INPUT_FLAG) {
|
|
4717
|
+
const next = rawArgs[i + 1];
|
|
4718
|
+
if (next === void 0) throw new FatalError("Missing value for --input: expected `--input key=value`");
|
|
4719
|
+
values.push(next);
|
|
4720
|
+
i += 1;
|
|
4721
|
+
} else if (arg.startsWith(INPUT_FLAG_EQ)) values.push(arg.slice(8));
|
|
4722
|
+
}
|
|
4723
|
+
return values;
|
|
4724
|
+
}
|
|
4725
|
+
/**
|
|
4726
|
+
* Parse `key=value` workflow-input strings (as collected by
|
|
4727
|
+
* {@link collectInputArgs}) into an inputs object. Throws {@link FatalError} on
|
|
4728
|
+
* a malformed entry -- no `=`, or an empty key.
|
|
4729
|
+
*/
|
|
4730
|
+
function parseInputs(raw) {
|
|
4731
|
+
const inputs = {};
|
|
4732
|
+
for (const value of raw) {
|
|
4733
|
+
const eq = value.indexOf("=");
|
|
4734
|
+
if (eq <= 0) throw new FatalError(`Invalid --input "${value}": expected key=value`);
|
|
4735
|
+
inputs[value.slice(0, eq)] = value.slice(eq + 1);
|
|
4736
|
+
}
|
|
4737
|
+
return inputs;
|
|
4738
|
+
}
|
|
4739
|
+
//#endregion
|
|
4740
|
+
//#region src/workflow-dispatch.ts
|
|
4741
|
+
/**
|
|
4742
|
+
* `bst-fj workflow:dispatch`: a thin command over `@bensandee/forgejo/actions`. Forgejo-only;
|
|
4743
|
+
* `gh workflow run` already serves GitHub.
|
|
4744
|
+
*/
|
|
4745
|
+
/** Dispatch a `workflow_dispatch` event on the current repository or on `options.repo`. */
|
|
4746
|
+
async function runWorkflowDispatch(fetcher, options) {
|
|
4747
|
+
const dispatched = await dispatchWorkflow(fetcher, requireForgejoTarget(options.cwd, void 0, "workflow:dispatch is Forgejo-only -- use gh workflow run on GitHub."), {
|
|
4748
|
+
...options.repo === void 0 ? {} : { repository: options.repo },
|
|
4749
|
+
workflow: options.workflow,
|
|
4750
|
+
ref: options.ref,
|
|
4751
|
+
inputs: options.inputs
|
|
4752
|
+
});
|
|
4753
|
+
return {
|
|
4754
|
+
status: "dispatched",
|
|
4755
|
+
host: "forgejo",
|
|
4756
|
+
repo: dispatched.repository,
|
|
4757
|
+
workflow: dispatched.workflow,
|
|
4758
|
+
ref: dispatched.ref,
|
|
4759
|
+
inputs: dispatched.inputs
|
|
4760
|
+
};
|
|
4761
|
+
}
|
|
4762
|
+
//#endregion
|
|
4763
|
+
//#region src/registry-actions.ts
|
|
4764
|
+
const actionsSubCommands = {
|
|
4765
|
+
"ci:log": ciLogCommand,
|
|
4766
|
+
"ci:runs": ciRunsCommand,
|
|
4767
|
+
"workflow:dispatch": defineCommand({
|
|
4768
|
+
meta: {
|
|
4769
|
+
name: "workflow:dispatch",
|
|
4770
|
+
description: "Trigger a workflow_dispatch event for a workflow in any repo on the same Forgejo host (Forgejo-only; use gh workflow run on GitHub). The token must have rights on --repo when it differs from the current repo."
|
|
4771
|
+
},
|
|
4772
|
+
args: {
|
|
4773
|
+
repo: {
|
|
4774
|
+
type: "string",
|
|
4775
|
+
description: "Target repository as owner/repo, not a URL (default: the current repo)",
|
|
4776
|
+
required: false
|
|
4777
|
+
},
|
|
4778
|
+
workflow: {
|
|
4779
|
+
type: "string",
|
|
4780
|
+
description: "Workflow filename (e.g. deploy.yml) or numeric ID",
|
|
4781
|
+
required: true
|
|
4782
|
+
},
|
|
4783
|
+
ref: {
|
|
4784
|
+
type: "string",
|
|
4785
|
+
description: "Git ref to dispatch on (default: main)",
|
|
4786
|
+
required: false
|
|
4787
|
+
},
|
|
4788
|
+
input: {
|
|
4789
|
+
type: "string",
|
|
4790
|
+
description: "Workflow input as key=value (repeatable)",
|
|
4791
|
+
required: false
|
|
4792
|
+
},
|
|
4793
|
+
json: {
|
|
4794
|
+
type: "boolean",
|
|
4795
|
+
description: "Emit one JSON document: status (dispatched), host (forgejo), repo (owner/repo), workflow, ref, inputs."
|
|
4796
|
+
},
|
|
4797
|
+
verbose: {
|
|
4798
|
+
type: "boolean",
|
|
4799
|
+
description: "Emit detailed debug output"
|
|
4800
|
+
}
|
|
4801
|
+
},
|
|
4802
|
+
async run({ args, rawArgs }) {
|
|
4803
|
+
const out = createCliOutput(args.json === true);
|
|
4804
|
+
if (!args.workflow) throw new FatalError("--workflow is required");
|
|
4805
|
+
const result = await runWorkflowDispatch(cliFetcher(), {
|
|
4806
|
+
cwd: process.cwd(),
|
|
4807
|
+
repo: args.repo ?? void 0,
|
|
4808
|
+
workflow: args.workflow,
|
|
4809
|
+
ref: args.ref ?? "main",
|
|
4810
|
+
inputs: parseInputs(collectInputArgs(rawArgs))
|
|
4811
|
+
});
|
|
4812
|
+
out.json(result);
|
|
4813
|
+
out.text(`Dispatched ${result.workflow} on ${result.repo} (ref: ${result.ref})`);
|
|
4814
|
+
}
|
|
4815
|
+
})
|
|
4816
|
+
};
|
|
4146
4817
|
z.object({
|
|
4147
4818
|
content: z.string(),
|
|
4148
4819
|
encoding: z.literal("base64")
|
|
@@ -4328,7 +4999,7 @@ const branchSubCommands = {
|
|
|
4328
4999
|
},
|
|
4329
5000
|
json: {
|
|
4330
5001
|
type: "boolean",
|
|
4331
|
-
description: "Emit JSON
|
|
5002
|
+
description: "Emit one JSON document: deleted."
|
|
4332
5003
|
},
|
|
4333
5004
|
verbose: {
|
|
4334
5005
|
type: "boolean",
|
|
@@ -4358,7 +5029,7 @@ const branchSubCommands = {
|
|
|
4358
5029
|
},
|
|
4359
5030
|
json: {
|
|
4360
5031
|
type: "boolean",
|
|
4361
|
-
description: "Emit JSON
|
|
5032
|
+
description: "Emit one JSON document: branches ([{name, protected, commitSha (or null), commitDate (or null)}]), truncated."
|
|
4362
5033
|
},
|
|
4363
5034
|
verbose: {
|
|
4364
5035
|
type: "boolean",
|
|
@@ -4914,7 +5585,12 @@ function forgejoIssueApi(fetcher, conn) {
|
|
|
4914
5585
|
const ISSUE_LIST_JSON_HELP = "Emit one JSON document: issues ([{number, title, state, url, author, labels, createdAt, updatedAt, closedAt (null while open), isPullRequest, assignees, attachments ([{id, name, size, createdAt, downloadUrl}])}]), truncated.";
|
|
4915
5586
|
const ISSUE_CREATE_JSON_HELP = "Emit one JSON document: number, url, assignees. A dry run reports number 0 and an empty url.";
|
|
4916
5587
|
const ISSUE_STATE_JSON_HELP = "Emit one JSON document: number, url, action (close | reopen), state ({previous, current}), changed, comment? ({posted}; present only when a comment was given).";
|
|
4917
|
-
const
|
|
5588
|
+
const ISSUE_COMMENT_JSON_HELP = "Emit one JSON document: number, url.";
|
|
5589
|
+
const ISSUE_UPDATE_JSON_HELP = "Emit one JSON document: number, url, previous ({title, body}), current ({title, body}), changed ([title | body]). A dry run reports current as it would be and writes nothing.";
|
|
5590
|
+
const ISSUE_DOWNLOAD_JSON_HELP = "Emit one JSON document: dir, files ([{name, size, path}]). A dry run lists the files it would write and writes nothing.";
|
|
5591
|
+
const ISSUE_LABEL_JSON_HELP = "Emit one JSON document: number, label, action (add | remove), url.";
|
|
5592
|
+
const ISSUE_SET_LABELS_JSON_HELP = "Emit one JSON document: number, labels, previous, added, removed, url. A dry run reports the change it would make and writes nothing.";
|
|
5593
|
+
const ISSUE_VIEW_JSON_HELP = "Emit one JSON document: number, title, state, url, author, labels, createdAt, updatedAt, closedAt, body, isPullRequest, assignees, attachments, comments ([{id, author, createdAt, updatedAt?, body, url?}]), commentsTruncated, pr? ({head ({ref, sha, repo (or null)}), base ({ref, sha, repo (or null)}), draft, merged, mergeable (true | false | null), state}), prUnavailable? ({reason}). On a pull request exactly one of pr and prUnavailable is present, and neither is otherwise; repo is null for a deleted fork; commentsTruncated is true when the comment read hit its cap of about 1000.";
|
|
4918
5594
|
/**
|
|
4919
5595
|
* The refusal for an {@link IssueNumberRequiredError}. `example` is the program's own complete
|
|
4920
5596
|
* invocation, for instance the program name, the command and a sample number, so every program
|
|
@@ -5361,7 +6037,7 @@ const issueCommentCommand = defineCommand({
|
|
|
5361
6037
|
},
|
|
5362
6038
|
json: {
|
|
5363
6039
|
type: "boolean",
|
|
5364
|
-
description:
|
|
6040
|
+
description: ISSUE_COMMENT_JSON_HELP
|
|
5365
6041
|
},
|
|
5366
6042
|
"dry-run": {
|
|
5367
6043
|
type: "boolean",
|
|
@@ -5618,7 +6294,7 @@ const issueDownloadCommand = defineCommand({
|
|
|
5618
6294
|
},
|
|
5619
6295
|
json: {
|
|
5620
6296
|
type: "boolean",
|
|
5621
|
-
description:
|
|
6297
|
+
description: ISSUE_DOWNLOAD_JSON_HELP
|
|
5622
6298
|
},
|
|
5623
6299
|
"dry-run": {
|
|
5624
6300
|
type: "boolean",
|
|
@@ -5858,7 +6534,7 @@ function defineIssueLabelCommand(action) {
|
|
|
5858
6534
|
},
|
|
5859
6535
|
json: {
|
|
5860
6536
|
type: "boolean",
|
|
5861
|
-
description:
|
|
6537
|
+
description: ISSUE_LABEL_JSON_HELP
|
|
5862
6538
|
},
|
|
5863
6539
|
"dry-run": {
|
|
5864
6540
|
type: "boolean",
|
|
@@ -5920,7 +6596,7 @@ const issueSetLabelsCommand = defineCommand({
|
|
|
5920
6596
|
},
|
|
5921
6597
|
json: {
|
|
5922
6598
|
type: "boolean",
|
|
5923
|
-
description:
|
|
6599
|
+
description: ISSUE_SET_LABELS_JSON_HELP
|
|
5924
6600
|
},
|
|
5925
6601
|
"dry-run": {
|
|
5926
6602
|
type: "boolean",
|
|
@@ -6202,7 +6878,7 @@ const issueUpdateCommand = defineCommand({
|
|
|
6202
6878
|
},
|
|
6203
6879
|
json: {
|
|
6204
6880
|
type: "boolean",
|
|
6205
|
-
description:
|
|
6881
|
+
description: ISSUE_UPDATE_JSON_HELP
|
|
6206
6882
|
},
|
|
6207
6883
|
"dry-run": {
|
|
6208
6884
|
type: "boolean",
|
|
@@ -6628,7 +7304,7 @@ const prChecksCommand = defineCommand({
|
|
|
6628
7304
|
},
|
|
6629
7305
|
json: {
|
|
6630
7306
|
type: "boolean",
|
|
6631
|
-
description: "Emit
|
|
7307
|
+
description: "Emit one JSON document: pr, repository, url, state, headSha, baseRef, status (green | red | pending | no-status | unparseable | head-moved | timeout), settled, combinedState, waited, elapsedMs, checks ([{context, name, state, verdict, description (or null), targetUrl (or null), updatedAt (or null), preexisting}]), baseline ({ref, sha?, status, reason?, redNames}), detail? (unparseable only), previousHeadSha? (head-moved only). no-status means no check was posted and is never green."
|
|
6632
7308
|
},
|
|
6633
7309
|
verbose: {
|
|
6634
7310
|
type: "boolean",
|
|
@@ -7588,7 +8264,7 @@ const prCreateCommand = defineCommand({
|
|
|
7588
8264
|
},
|
|
7589
8265
|
json: {
|
|
7590
8266
|
type: "boolean",
|
|
7591
|
-
description: "Emit
|
|
8267
|
+
description: "Emit one JSON document: action (created | updated | would-create | would-update), number (or null), url (or null), branch, base, title, body, droppedRefs? ([{text, issue}]), dryRun. A create or would-create carries droppedRefs; on a dry run, number and url are null."
|
|
7592
8268
|
},
|
|
7593
8269
|
verbose: {
|
|
7594
8270
|
type: "boolean",
|
|
@@ -7696,7 +8372,7 @@ const prFilesCommand = defineCommand({
|
|
|
7696
8372
|
},
|
|
7697
8373
|
json: {
|
|
7698
8374
|
type: "boolean",
|
|
7699
|
-
description: "Emit JSON
|
|
8375
|
+
description: "Emit one JSON document: files ([{filename, status (or null), additions, deletions}]), truncated."
|
|
7700
8376
|
},
|
|
7701
8377
|
verbose: {
|
|
7702
8378
|
type: "boolean",
|
|
@@ -7855,7 +8531,8 @@ const MergeGateSchema = z.object({
|
|
|
7855
8531
|
merge_base: z.string(),
|
|
7856
8532
|
changed_files: z.number().nullish(),
|
|
7857
8533
|
base: z.object({ ref: z.string() }),
|
|
7858
|
-
labels: z.array(z.object({ name: z.string() })).nullable()
|
|
8534
|
+
labels: z.array(z.object({ name: z.string() })).nullable(),
|
|
8535
|
+
updated_at: z.string().nullish()
|
|
7859
8536
|
});
|
|
7860
8537
|
/**
|
|
7861
8538
|
* Read a PR's live open/draft/mergeable state and current head SHA in one `/pulls/<n>` GET --
|
|
@@ -7880,7 +8557,8 @@ async function readMergeGate(executor, conn, prNumber) {
|
|
|
7880
8557
|
mergeBase: parsed.data.merge_base,
|
|
7881
8558
|
changedFiles: parsed.data.changed_files ?? null,
|
|
7882
8559
|
baseRef: parsed.data.base.ref,
|
|
7883
|
-
labels: (parsed.data.labels ?? []).map((label) => label.name)
|
|
8560
|
+
labels: (parsed.data.labels ?? []).map((label) => label.name),
|
|
8561
|
+
updatedAt: parsed.data.updated_at ?? null
|
|
7884
8562
|
};
|
|
7885
8563
|
}
|
|
7886
8564
|
/**
|
|
@@ -8046,7 +8724,7 @@ const prSubCommands = {
|
|
|
8046
8724
|
},
|
|
8047
8725
|
json: {
|
|
8048
8726
|
type: "boolean",
|
|
8049
|
-
description: "Emit JSON on
|
|
8727
|
+
description: "Emit one JSON document: number, merged, method (merge | rebase | squash), headSha, verdict (mergeable), checks (green | red | pending | no-status | unparseable). merged is false on a dry run; a refusal is an error, never this document."
|
|
8050
8728
|
},
|
|
8051
8729
|
verbose: {
|
|
8052
8730
|
type: "boolean",
|
|
@@ -8100,7 +8778,7 @@ const prSubCommands = {
|
|
|
8100
8778
|
},
|
|
8101
8779
|
json: {
|
|
8102
8780
|
type: "boolean",
|
|
8103
|
-
description: "Emit
|
|
8781
|
+
description: "Emit one JSON document: action (updated | would-update), number (or null), url (or null), branch, base, title, body, dryRun, rewrites. On a dry run, number and url are null."
|
|
8104
8782
|
},
|
|
8105
8783
|
verbose: {
|
|
8106
8784
|
type: "boolean",
|
|
@@ -8155,7 +8833,7 @@ setForgejoLogger(rootLogger);
|
|
|
8155
8833
|
await runCli({
|
|
8156
8834
|
name: "bst-fj",
|
|
8157
8835
|
version: BST_FJ_VERSION,
|
|
8158
|
-
description: "Forgejo API commands: labels, secrets, webhooks, releases, issues, PRs, branches, workflows and commit checks",
|
|
8836
|
+
description: "Forgejo API commands: labels, secrets, webhooks, releases, issues, PRs, branches, workflows, CI runs and logs, and commit checks",
|
|
8159
8837
|
banner: void 0,
|
|
8160
8838
|
subCommands: bstFjSubCommands,
|
|
8161
8839
|
describeError: describeForgeError
|