@brainervirus/workit-cli 2.3.0 → 2.4.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/dist/index.js +2072 -182
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -73016,15 +73016,1851 @@ var init_check = __esm(() => {
|
|
|
73016
73016
|
run12 = notImplemented("check", "S9b");
|
|
73017
73017
|
});
|
|
73018
73018
|
|
|
73019
|
+
// packages/workit-core/src/git/rev.ts
|
|
73020
|
+
import { spawnSync as spawnSync13 } from "node:child_process";
|
|
73021
|
+
import { createHash as createHash12 } from "node:crypto";
|
|
73022
|
+
import fs15 from "node:fs";
|
|
73023
|
+
import os12 from "node:os";
|
|
73024
|
+
import path34 from "node:path";
|
|
73025
|
+
function networkGitInvocation(cwd, args, timeoutMs) {
|
|
73026
|
+
const seconds = Math.max(1, Math.ceil(timeoutMs / 1000));
|
|
73027
|
+
const env = {
|
|
73028
|
+
...process.env,
|
|
73029
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
73030
|
+
GIT_ASKPASS: "",
|
|
73031
|
+
SSH_ASKPASS: "",
|
|
73032
|
+
SSH_ASKPASS_REQUIRE: "never",
|
|
73033
|
+
GCM_INTERACTIVE: "never"
|
|
73034
|
+
};
|
|
73035
|
+
const base = process.env.GIT_SSH_COMMAND || line(git(cwd, ["config", "--get", "core.sshCommand"])) || (process.env.GIT_SSH ? null : "ssh");
|
|
73036
|
+
if (base)
|
|
73037
|
+
env.GIT_SSH_COMMAND = `${base} -o BatchMode=yes -o ConnectTimeout=${Math.min(10, seconds)}`;
|
|
73038
|
+
return {
|
|
73039
|
+
args: ["-c", "http.lowSpeedLimit=1", "-c", `http.lowSpeedTime=${seconds}`, ...args],
|
|
73040
|
+
env
|
|
73041
|
+
};
|
|
73042
|
+
}
|
|
73043
|
+
function redactRemote(raw) {
|
|
73044
|
+
const value = raw.trim();
|
|
73045
|
+
if (!value)
|
|
73046
|
+
return UNPARSEABLE;
|
|
73047
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*::/u.test(value))
|
|
73048
|
+
return UNPARSEABLE;
|
|
73049
|
+
if (/^[A-Za-z]:[\\/]/u.test(value) || value.startsWith("/") || value.startsWith("."))
|
|
73050
|
+
return value;
|
|
73051
|
+
if (!/[:@]/u.test(value))
|
|
73052
|
+
return value;
|
|
73053
|
+
if (!value.includes("://")) {
|
|
73054
|
+
if (/^[^/@]*:[^/]*@/u.test(value))
|
|
73055
|
+
return UNPARSEABLE;
|
|
73056
|
+
const scp = /^(?:([^@\s/:]+)@)?([^:/\s]+):([^?#]+)/u.exec(value);
|
|
73057
|
+
return scp ? `${scp[1] ? `${scp[1]}@` : ""}${scp[2]}:${scp[3]}` : UNPARSEABLE;
|
|
73058
|
+
}
|
|
73059
|
+
try {
|
|
73060
|
+
const url = new URL(value);
|
|
73061
|
+
const ssh = /^(?:ssh|git\+ssh|ssh\+git):$/u.test(url.protocol);
|
|
73062
|
+
url.password = "";
|
|
73063
|
+
if (!ssh)
|
|
73064
|
+
url.username = "";
|
|
73065
|
+
url.search = "";
|
|
73066
|
+
url.hash = "";
|
|
73067
|
+
return url.toString();
|
|
73068
|
+
} catch {
|
|
73069
|
+
return UNPARSEABLE;
|
|
73070
|
+
}
|
|
73071
|
+
}
|
|
73072
|
+
function headSha(cwd) {
|
|
73073
|
+
return line(git(cwd, ["rev-parse", "--verify", "-q", "HEAD^{commit}"]));
|
|
73074
|
+
}
|
|
73075
|
+
function worktreeTree(cwd, options = {}) {
|
|
73076
|
+
const top = line(git(cwd, ["rev-parse", "--show-toplevel"]));
|
|
73077
|
+
const indexPath = line(git(cwd, ["rev-parse", "--path-format=absolute", "--git-path", "index"]));
|
|
73078
|
+
if (!top || !indexPath)
|
|
73079
|
+
return null;
|
|
73080
|
+
const cap = options.maxUntrackedBytes ?? MAX_UNTRACKED_BYTES;
|
|
73081
|
+
const timeoutMs = options.timeoutMs ?? GIT_TIMEOUTS.worktree;
|
|
73082
|
+
const scratch = fs15.mkdtempSync(path34.join(os12.tmpdir(), "workit-tree-"));
|
|
73083
|
+
try {
|
|
73084
|
+
const tempIndex = path34.join(scratch, "index");
|
|
73085
|
+
if (fs15.existsSync(indexPath))
|
|
73086
|
+
seedIndex(indexPath, tempIndex);
|
|
73087
|
+
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
73088
|
+
const untracked = git(top, ["ls-files", "--others", "--exclude-standard", "-z"], {
|
|
73089
|
+
env,
|
|
73090
|
+
timeoutMs
|
|
73091
|
+
});
|
|
73092
|
+
if (!untracked.ok)
|
|
73093
|
+
return null;
|
|
73094
|
+
const skipped = [];
|
|
73095
|
+
for (const file of untracked.stdout.split("\x00").filter(Boolean)) {
|
|
73096
|
+
try {
|
|
73097
|
+
const stat = fs15.statSync(path34.join(top, file));
|
|
73098
|
+
if (stat.isFile() && stat.size > cap)
|
|
73099
|
+
skipped.push({ path: file, size: stat.size, mtimeMs: Math.trunc(stat.mtimeMs) });
|
|
73100
|
+
} catch {}
|
|
73101
|
+
}
|
|
73102
|
+
skipped.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
73103
|
+
const added = skipped.length ? git(top, ["add", "-A", "--pathspec-from-file=-", "--pathspec-file-nul"], {
|
|
73104
|
+
env,
|
|
73105
|
+
timeoutMs,
|
|
73106
|
+
input: [".", ...skipped.map((file) => `:(exclude,literal)${file.path}`)].join("\x00")
|
|
73107
|
+
}) : git(top, ["add", "-A"], { env, timeoutMs });
|
|
73108
|
+
if (!added.ok)
|
|
73109
|
+
return null;
|
|
73110
|
+
const tree = line(git(top, ["write-tree"], { env, timeoutMs }));
|
|
73111
|
+
if (!tree)
|
|
73112
|
+
return null;
|
|
73113
|
+
const base = line(git(top, ["rev-parse", "--verify", "-q", "HEAD^{tree}"])) ?? emptyTree(top);
|
|
73114
|
+
const key = skipped.length ? `sha256:${createHash12("sha256").update(JSON.stringify({ tree, skipped })).digest("hex")}` : tree;
|
|
73115
|
+
return { tree, key, dirty: tree !== base || skipped.length > 0, skipped };
|
|
73116
|
+
} finally {
|
|
73117
|
+
fs15.rmSync(scratch, { recursive: true, force: true });
|
|
73118
|
+
}
|
|
73119
|
+
}
|
|
73120
|
+
function patchId(cwd, base, head) {
|
|
73121
|
+
if (!safeArg(base) || !safeArg(head))
|
|
73122
|
+
return null;
|
|
73123
|
+
const diff = git(cwd, [
|
|
73124
|
+
"diff",
|
|
73125
|
+
"-U3",
|
|
73126
|
+
"--diff-algorithm=myers",
|
|
73127
|
+
"--indent-heuristic",
|
|
73128
|
+
"--no-color",
|
|
73129
|
+
"--no-ext-diff",
|
|
73130
|
+
"--no-textconv",
|
|
73131
|
+
"--no-renames",
|
|
73132
|
+
"--full-index",
|
|
73133
|
+
"--src-prefix=a/",
|
|
73134
|
+
"--dst-prefix=b/",
|
|
73135
|
+
`${base}...${head}`,
|
|
73136
|
+
"--"
|
|
73137
|
+
]);
|
|
73138
|
+
if (!diff.ok || !diff.stdout.trim())
|
|
73139
|
+
return null;
|
|
73140
|
+
const id = git(cwd, ["patch-id", "--stable"], { input: diff.stdout });
|
|
73141
|
+
return line(id)?.split(/\s+/u)[0] ?? null;
|
|
73142
|
+
}
|
|
73143
|
+
function fetchRefs(cwd, remote, refspecs, options = {}) {
|
|
73144
|
+
if (!safeArg(remote) || refspecs.length === 0 || !refspecs.every((spec) => safeArg(spec.replace(/^\+/u, ""))))
|
|
73145
|
+
return { ok: false, code: "invalid_input", error: "remote and refspecs must not start with -" };
|
|
73146
|
+
const timeoutMs = options.timeoutMs ?? GIT_TIMEOUTS.network;
|
|
73147
|
+
const fetched = gitNetwork(cwd, [
|
|
73148
|
+
"fetch",
|
|
73149
|
+
"--no-tags",
|
|
73150
|
+
"--no-recurse-submodules",
|
|
73151
|
+
"--no-write-fetch-head",
|
|
73152
|
+
"--quiet",
|
|
73153
|
+
remote,
|
|
73154
|
+
...refspecs
|
|
73155
|
+
], timeoutMs);
|
|
73156
|
+
if (fetched.ok)
|
|
73157
|
+
return { ok: true };
|
|
73158
|
+
if (/\.lock'?:? File exists|unable to lock|another git process/iu.test(fetched.stderr))
|
|
73159
|
+
return {
|
|
73160
|
+
ok: false,
|
|
73161
|
+
code: "busy",
|
|
73162
|
+
error: `git fetch ${redactRemote(remote)}: the repository is locked by another git process`
|
|
73163
|
+
};
|
|
73164
|
+
return {
|
|
73165
|
+
ok: false,
|
|
73166
|
+
code: "unavailable",
|
|
73167
|
+
error: fetched.timedOut ? `git fetch ${redactRemote(remote)} timed out after ${timeoutMs} ms` : `git fetch ${redactRemote(remote)} failed (unreachable, not authorized, or ref missing)`
|
|
73168
|
+
};
|
|
73169
|
+
}
|
|
73170
|
+
function remoteNames(cwd) {
|
|
73171
|
+
return (git(cwd, ["remote"]).stdout ?? "").split(/\r?\n/u).map((value) => value.trim()).filter(Boolean);
|
|
73172
|
+
}
|
|
73173
|
+
function gitCommonDir(cwd) {
|
|
73174
|
+
return line(git(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]));
|
|
73175
|
+
}
|
|
73176
|
+
function hasCommit(cwd, rev) {
|
|
73177
|
+
if (!safeArg(rev))
|
|
73178
|
+
return false;
|
|
73179
|
+
return git(cwd, ["cat-file", "-e", `${rev}^{commit}`]).ok;
|
|
73180
|
+
}
|
|
73181
|
+
function resolveRef(cwd, ref) {
|
|
73182
|
+
if (!safeArg(ref))
|
|
73183
|
+
return null;
|
|
73184
|
+
return line(git(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]));
|
|
73185
|
+
}
|
|
73186
|
+
function aheadBehind(cwd, base, head) {
|
|
73187
|
+
if (!safeArg(base) || !safeArg(head))
|
|
73188
|
+
return null;
|
|
73189
|
+
const counted = line(git(cwd, ["rev-list", "--left-right", "--count", `${base}...${head}`]));
|
|
73190
|
+
const match = counted ? /^(\d+)\s+(\d+)$/u.exec(counted) : null;
|
|
73191
|
+
if (!match)
|
|
73192
|
+
return null;
|
|
73193
|
+
const upToDate = git(cwd, ["merge-base", "--is-ancestor", base, head]).ok;
|
|
73194
|
+
return { behind: Number(match[1]), ahead: Number(match[2]), upToDate };
|
|
73195
|
+
}
|
|
73196
|
+
function currentBranch2(cwd) {
|
|
73197
|
+
return line(git(cwd, ["symbolic-ref", "--short", "-q", "HEAD"]));
|
|
73198
|
+
}
|
|
73199
|
+
function pushRemoteName(cwd, branch) {
|
|
73200
|
+
const name = branch ?? currentBranch2(cwd);
|
|
73201
|
+
const remotes = remoteNames(cwd);
|
|
73202
|
+
const candidates = [
|
|
73203
|
+
name ? config2(cwd, `branch.${name}.pushRemote`) : null,
|
|
73204
|
+
config2(cwd, "remote.pushDefault"),
|
|
73205
|
+
name ? config2(cwd, `branch.${name}.remote`) : null
|
|
73206
|
+
];
|
|
73207
|
+
for (const candidate of candidates)
|
|
73208
|
+
if (candidate && candidate !== "." && remotes.includes(candidate))
|
|
73209
|
+
return candidate;
|
|
73210
|
+
if (remotes.includes("origin"))
|
|
73211
|
+
return "origin";
|
|
73212
|
+
return remotes.length === 1 ? remotes[0] : null;
|
|
73213
|
+
}
|
|
73214
|
+
function pushUrl(cwd, remote) {
|
|
73215
|
+
if (!safeArg(remote))
|
|
73216
|
+
return null;
|
|
73217
|
+
const urls = git(cwd, ["remote", "get-url", "--push", "--all", remote]).stdout.split(/\r?\n/u).map((value) => value.trim()).filter(Boolean);
|
|
73218
|
+
return urls.length === 1 ? urls[0] : null;
|
|
73219
|
+
}
|
|
73220
|
+
function parseRemoteUrl(raw) {
|
|
73221
|
+
const value = raw.trim();
|
|
73222
|
+
if (!value || /^[A-Za-z][A-Za-z0-9+.-]*::/u.test(value))
|
|
73223
|
+
return null;
|
|
73224
|
+
if (/^[A-Za-z]:[\\/]/u.test(value) || value.startsWith("/") || value.startsWith(".")) {
|
|
73225
|
+
return { protocol: "file", user: null, host: "", port: null, path: value };
|
|
73226
|
+
}
|
|
73227
|
+
if (!value.includes("://")) {
|
|
73228
|
+
if (/^[^/@]*:[^/]*@/u.test(value))
|
|
73229
|
+
return null;
|
|
73230
|
+
const scp = /^(?:([^@\s/:]+)@)?([^:/\s]+):(.+)$/u.exec(value);
|
|
73231
|
+
if (!scp)
|
|
73232
|
+
return null;
|
|
73233
|
+
return {
|
|
73234
|
+
protocol: "ssh",
|
|
73235
|
+
user: scp[1] ?? null,
|
|
73236
|
+
host: scp[2],
|
|
73237
|
+
port: null,
|
|
73238
|
+
path: cleanPath(scp[3])
|
|
73239
|
+
};
|
|
73240
|
+
}
|
|
73241
|
+
let url;
|
|
73242
|
+
try {
|
|
73243
|
+
url = new URL(value);
|
|
73244
|
+
} catch {
|
|
73245
|
+
return null;
|
|
73246
|
+
}
|
|
73247
|
+
const scheme = url.protocol.replace(/:$/u, "");
|
|
73248
|
+
const protocol = scheme === "ssh" || scheme === "git+ssh" || scheme === "ssh+git" ? "ssh" : scheme === "https" || scheme === "http" || scheme === "git" || scheme === "file" ? scheme : null;
|
|
73249
|
+
if (!protocol)
|
|
73250
|
+
return null;
|
|
73251
|
+
return {
|
|
73252
|
+
protocol,
|
|
73253
|
+
user: protocol === "ssh" && url.username ? decodeURIComponent(url.username) : null,
|
|
73254
|
+
host: url.hostname.replace(/\.$/u, ""),
|
|
73255
|
+
port: url.port || null,
|
|
73256
|
+
path: cleanPath(decodeURIComponent(url.pathname))
|
|
73257
|
+
};
|
|
73258
|
+
}
|
|
73259
|
+
function readSshConfig(home = os12.homedir()) {
|
|
73260
|
+
try {
|
|
73261
|
+
return { text: fs15.readFileSync(path34.join(home, ".ssh", "config"), "utf8"), home };
|
|
73262
|
+
} catch {
|
|
73263
|
+
return { text: "", home };
|
|
73264
|
+
}
|
|
73265
|
+
}
|
|
73266
|
+
function resolveSshHost(alias, sshConfig) {
|
|
73267
|
+
const source = typeof sshConfig === "string" ? { text: sshConfig, home: os12.homedir() } : sshConfig;
|
|
73268
|
+
let hostname = null;
|
|
73269
|
+
let port = null;
|
|
73270
|
+
const evaluate = (text, initiallyActive, neverMatch, depth) => {
|
|
73271
|
+
let active = initiallyActive;
|
|
73272
|
+
for (const { key, args } of directives(text)) {
|
|
73273
|
+
if (key === "host") {
|
|
73274
|
+
active = !neverMatch && hostMatches(args, alias);
|
|
73275
|
+
continue;
|
|
73276
|
+
}
|
|
73277
|
+
if (key === "match") {
|
|
73278
|
+
active = false;
|
|
73279
|
+
continue;
|
|
73280
|
+
}
|
|
73281
|
+
if (key === "include") {
|
|
73282
|
+
if (depth >= 16)
|
|
73283
|
+
continue;
|
|
73284
|
+
for (const spec of args)
|
|
73285
|
+
for (const file of includeFiles(spec, source.home)) {
|
|
73286
|
+
let included;
|
|
73287
|
+
try {
|
|
73288
|
+
included = fs15.readFileSync(file, "utf8");
|
|
73289
|
+
} catch {
|
|
73290
|
+
continue;
|
|
73291
|
+
}
|
|
73292
|
+
evaluate(included, active, neverMatch || !active, depth + 1);
|
|
73293
|
+
}
|
|
73294
|
+
continue;
|
|
73295
|
+
}
|
|
73296
|
+
if (!active || args.length === 0)
|
|
73297
|
+
continue;
|
|
73298
|
+
if (key === "hostname" && hostname === null)
|
|
73299
|
+
hostname = args[0].replace(/%(%|h)/gu, (_, token) => token === "h" ? alias : "%");
|
|
73300
|
+
if (key === "port" && port === null)
|
|
73301
|
+
port = args[0];
|
|
73302
|
+
}
|
|
73303
|
+
};
|
|
73304
|
+
evaluate(source.text, true, false, 0);
|
|
73305
|
+
return { hostname: (hostname ?? alias).replace(/\.$/u, ""), port };
|
|
73306
|
+
}
|
|
73307
|
+
function deriveForge(url, options = {}) {
|
|
73308
|
+
const parsed = parseRemoteUrl(url);
|
|
73309
|
+
if (!parsed || parsed.protocol === "file" || !parsed.host)
|
|
73310
|
+
return null;
|
|
73311
|
+
const segments = parsed.path.split("/").filter(Boolean);
|
|
73312
|
+
if (segments.length < 2)
|
|
73313
|
+
return null;
|
|
73314
|
+
const hostname = (parsed.protocol === "ssh" ? resolveSshHost(parsed.host, options.sshConfig ?? readSshConfig()).hostname : parsed.host).toLowerCase();
|
|
73315
|
+
const withPort = parsed.protocol !== "ssh" && parsed.port ? `${hostname}:${parsed.port}` : hostname;
|
|
73316
|
+
const base = { host: parsed.host, hostname, repo: segments.join("/") };
|
|
73317
|
+
const known = KNOWN_HOSTS[hostname];
|
|
73318
|
+
if (known) {
|
|
73319
|
+
if (known.kind === "github" && segments.length !== 2)
|
|
73320
|
+
return null;
|
|
73321
|
+
return { ...base, kind: known.kind, apiHost: known.apiHost, via: "known_host" };
|
|
73322
|
+
}
|
|
73323
|
+
for (const kind of ["github", "gitlab"]) {
|
|
73324
|
+
const hosts = new Set((options.hosts?.[kind] ?? []).map(normalizeHost));
|
|
73325
|
+
if (hosts.has(withPort) || hosts.has(hostname)) {
|
|
73326
|
+
const apiHost = hosts.has(withPort) ? withPort : hostname;
|
|
73327
|
+
return { ...base, kind, apiHost, via: "configured_host" };
|
|
73328
|
+
}
|
|
73329
|
+
}
|
|
73330
|
+
const label = hostname.split(".")[0];
|
|
73331
|
+
if ((label === "github" || label === "gitlab") && hostname.includes("."))
|
|
73332
|
+
return { ...base, kind: label, apiHost: withPort, via: "host_name" };
|
|
73333
|
+
return null;
|
|
73334
|
+
}
|
|
73335
|
+
function pushForge(cwd, options = {}) {
|
|
73336
|
+
const remote = pushRemoteName(cwd, options.branch);
|
|
73337
|
+
if (!remote)
|
|
73338
|
+
return {
|
|
73339
|
+
ok: false,
|
|
73340
|
+
code: "not_found",
|
|
73341
|
+
error: "no push remote is configured for this branch",
|
|
73342
|
+
unblock: "git remote add origin <url>"
|
|
73343
|
+
};
|
|
73344
|
+
const url = pushUrl(cwd, remote);
|
|
73345
|
+
if (!url)
|
|
73346
|
+
return {
|
|
73347
|
+
ok: false,
|
|
73348
|
+
code: "unavailable",
|
|
73349
|
+
error: `remote "${remote}" has no single push URL`,
|
|
73350
|
+
unblock: `git remote get-url --push --all ${remote} # keep exactly one push URL`
|
|
73351
|
+
};
|
|
73352
|
+
const forge = deriveForge(url, options);
|
|
73353
|
+
if (!forge) {
|
|
73354
|
+
const host = parseRemoteUrl(url)?.host || null;
|
|
73355
|
+
return {
|
|
73356
|
+
ok: false,
|
|
73357
|
+
code: "unavailable",
|
|
73358
|
+
error: host ? `cannot tell whether push host "${host}" is GitHub or GitLab` : `push URL ${redactRemote(url)} of remote "${remote}" is not a GitHub/GitLab URL`,
|
|
73359
|
+
unblock: host ? `map the alias in ~/.ssh/config (Host ${host} / HostName github.com|gitlab.com) or set github.host / gitlab.host in ~/.config/workit/vcs.json` : `git remote set-url --push ${remote} <ssh-or-https-url>`
|
|
73360
|
+
};
|
|
73361
|
+
}
|
|
73362
|
+
return { ok: true, remote, url: redactRemote(url), forge };
|
|
73363
|
+
}
|
|
73364
|
+
function forgeConflict(derived, configuredProvider) {
|
|
73365
|
+
const configured = configuredProvider?.trim().toLowerCase();
|
|
73366
|
+
if (!configured || configured === derived.kind)
|
|
73367
|
+
return null;
|
|
73368
|
+
return {
|
|
73369
|
+
code: "blocked",
|
|
73370
|
+
error: `forge_mismatch: push remote ${derived.host} is ${derived.kind} but the workspace vcs.provider is ${configured}`,
|
|
73371
|
+
unblock: `set vcs.provider to "${derived.kind}" for this repo in ~/.config/workit/workspaces.json (workit-github-override), or push to a ${configured} remote`
|
|
73372
|
+
};
|
|
73373
|
+
}
|
|
73374
|
+
var MAX_BUFFER, GIT_TIMEOUTS, git = (cwd, args, options = {}) => {
|
|
73375
|
+
const result = spawnSync13("git", args, {
|
|
73376
|
+
cwd,
|
|
73377
|
+
encoding: "utf8",
|
|
73378
|
+
env: options.env ?? process.env,
|
|
73379
|
+
input: options.input,
|
|
73380
|
+
maxBuffer: MAX_BUFFER,
|
|
73381
|
+
timeout: options.timeoutMs ?? GIT_TIMEOUTS.local,
|
|
73382
|
+
killSignal: "SIGKILL",
|
|
73383
|
+
windowsHide: true
|
|
73384
|
+
});
|
|
73385
|
+
const error = result.error;
|
|
73386
|
+
return {
|
|
73387
|
+
ok: result.status === 0,
|
|
73388
|
+
stdout: result.stdout ?? "",
|
|
73389
|
+
stderr: result.stderr ?? "",
|
|
73390
|
+
timedOut: error?.code === "ETIMEDOUT"
|
|
73391
|
+
};
|
|
73392
|
+
}, line = (run) => {
|
|
73393
|
+
if (!run.ok)
|
|
73394
|
+
return null;
|
|
73395
|
+
const value = run.stdout.trim();
|
|
73396
|
+
return value ? value : null;
|
|
73397
|
+
}, safeArg = (value) => value.length > 0 && !value.startsWith("-"), WATCHDOG = `
|
|
73398
|
+
const cp = require("node:child_process");
|
|
73399
|
+
const { ms, args } = JSON.parse(process.env.WORKIT_GIT_WATCHDOG);
|
|
73400
|
+
delete process.env.WORKIT_GIT_WATCHDOG;
|
|
73401
|
+
const win = process.platform === "win32";
|
|
73402
|
+
const child = cp.spawn("git", args, { stdio: "inherit", detached: !win, windowsHide: true });
|
|
73403
|
+
const killTree = () => {
|
|
73404
|
+
if (!child.pid) return;
|
|
73405
|
+
if (win) cp.spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
73406
|
+
else try { process.kill(-child.pid, "SIGKILL"); } catch {}
|
|
73407
|
+
};
|
|
73408
|
+
let timedOut = false;
|
|
73409
|
+
const timer = setTimeout(() => { timedOut = true; killTree(); }, ms);
|
|
73410
|
+
child.on("error", () => { clearTimeout(timer); process.exit(127); });
|
|
73411
|
+
child.on("exit", (code) => {
|
|
73412
|
+
clearTimeout(timer);
|
|
73413
|
+
if (!win) killTree();
|
|
73414
|
+
process.exit(timedOut ? 124 : code ?? 1);
|
|
73415
|
+
});
|
|
73416
|
+
`, WATCHDOG_TIMEOUT_EXIT = 124, gitNetwork = (cwd, args, timeoutMs) => {
|
|
73417
|
+
const invocation = networkGitInvocation(cwd, args, timeoutMs);
|
|
73418
|
+
const result = spawnSync13(process.execPath, ["-e", WATCHDOG], {
|
|
73419
|
+
cwd,
|
|
73420
|
+
encoding: "utf8",
|
|
73421
|
+
env: {
|
|
73422
|
+
...invocation.env,
|
|
73423
|
+
WORKIT_GIT_WATCHDOG: JSON.stringify({ ms: timeoutMs, args: invocation.args })
|
|
73424
|
+
},
|
|
73425
|
+
maxBuffer: MAX_BUFFER,
|
|
73426
|
+
timeout: timeoutMs + 1e4,
|
|
73427
|
+
killSignal: "SIGKILL",
|
|
73428
|
+
windowsHide: true
|
|
73429
|
+
});
|
|
73430
|
+
const error = result.error;
|
|
73431
|
+
return {
|
|
73432
|
+
ok: result.status === 0,
|
|
73433
|
+
stdout: result.stdout ?? "",
|
|
73434
|
+
stderr: result.stderr ?? "",
|
|
73435
|
+
timedOut: result.status === WATCHDOG_TIMEOUT_EXIT || error?.code === "ETIMEDOUT"
|
|
73436
|
+
};
|
|
73437
|
+
}, UNPARSEABLE = "<unparseable remote>", emptyTree = (cwd) => line(git(cwd, ["mktree"], { input: "" })), MAX_UNTRACKED_BYTES, seedIndex = (from, to) => {
|
|
73438
|
+
fs15.copyFileSync(from, to);
|
|
73439
|
+
const { atimeNs, mtimeNs } = fs15.statSync(from, { bigint: true });
|
|
73440
|
+
const seconds = (ns) => (Number(ns / 1000n) - 1) / 1e6;
|
|
73441
|
+
fs15.utimesSync(to, seconds(atimeNs), seconds(mtimeNs));
|
|
73442
|
+
}, config2 = (cwd, key) => line(git(cwd, ["config", "--get", key])), cleanPath = (value) => value.replace(/^\/+/u, "").replace(/\/+$/u, "").replace(/\.git$/u, ""), globToRegExp2 = (pattern) => new RegExp(`^${pattern.split("").map((char) => char === "*" ? ".*" : char === "?" ? "." : char.replace(/[.+^${}()|[\]\\]/gu, "\\$&")).join("")}$`, "iu"), hostMatches = (patterns, host) => {
|
|
73443
|
+
let matched = false;
|
|
73444
|
+
for (const pattern of patterns) {
|
|
73445
|
+
const negated = pattern.startsWith("!");
|
|
73446
|
+
if (globToRegExp2(negated ? pattern.slice(1) : pattern).test(host)) {
|
|
73447
|
+
if (negated)
|
|
73448
|
+
return false;
|
|
73449
|
+
matched = true;
|
|
73450
|
+
}
|
|
73451
|
+
}
|
|
73452
|
+
return matched;
|
|
73453
|
+
}, tokens = (rest) => [...rest.matchAll(/"([^"]*)"|(\S+)/gu)].map((match) => match[1] ?? match[2]), directives = (text) => text.split(/\r?\n/u).flatMap((raw) => {
|
|
73454
|
+
const trimmed = raw.trim();
|
|
73455
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
73456
|
+
return [];
|
|
73457
|
+
const match = /^(\S+?)(?:\s*=\s*|\s+)(.*)$/u.exec(trimmed);
|
|
73458
|
+
if (!match)
|
|
73459
|
+
return [];
|
|
73460
|
+
return [{ key: match[1].toLowerCase(), args: tokens(match[2]) }];
|
|
73461
|
+
}), includeFiles = (spec, home) => {
|
|
73462
|
+
const expanded = spec.startsWith("~") ? path34.join(home, spec.slice(1)) : spec;
|
|
73463
|
+
const absolute = path34.isAbsolute(expanded) ? expanded : path34.join(home, ".ssh", expanded);
|
|
73464
|
+
const dir = path34.dirname(absolute);
|
|
73465
|
+
const base = path34.basename(absolute);
|
|
73466
|
+
if (!/[*?]/u.test(base))
|
|
73467
|
+
return [absolute];
|
|
73468
|
+
try {
|
|
73469
|
+
const pattern = globToRegExp2(base);
|
|
73470
|
+
return fs15.readdirSync(dir).filter((name) => pattern.test(name)).toSorted().map((name) => path34.join(dir, name));
|
|
73471
|
+
} catch {
|
|
73472
|
+
return [];
|
|
73473
|
+
}
|
|
73474
|
+
}, KNOWN_HOSTS, normalizeHost = (value) => value.trim().toLowerCase().replace(/^https?:\/\//u, "").replace(/\/.*$/u, "").replace(/\.$/u, "");
|
|
73475
|
+
var init_rev = __esm(() => {
|
|
73476
|
+
MAX_BUFFER = 256 * 1024 * 1024;
|
|
73477
|
+
GIT_TIMEOUTS = { local: 20000, worktree: 120000, network: 15000 };
|
|
73478
|
+
MAX_UNTRACKED_BYTES = 5 * 1024 * 1024;
|
|
73479
|
+
KNOWN_HOSTS = {
|
|
73480
|
+
"github.com": { kind: "github", apiHost: "github.com" },
|
|
73481
|
+
"ssh.github.com": { kind: "github", apiHost: "github.com" },
|
|
73482
|
+
"gitlab.com": { kind: "gitlab", apiHost: "gitlab.com" },
|
|
73483
|
+
"altssh.gitlab.com": { kind: "gitlab", apiHost: "gitlab.com" }
|
|
73484
|
+
};
|
|
73485
|
+
});
|
|
73486
|
+
|
|
73487
|
+
// packages/workit-core/src/forge/reruns.ts
|
|
73488
|
+
import fs16 from "node:fs";
|
|
73489
|
+
import path35 from "node:path";
|
|
73490
|
+
function rerunLogPath(cwd) {
|
|
73491
|
+
const common = gitCommonDir(cwd);
|
|
73492
|
+
return common ? path35.join(common, "workit", "ci-reruns.jsonl") : null;
|
|
73493
|
+
}
|
|
73494
|
+
function rerunCounts(cwd, key) {
|
|
73495
|
+
const counts = new Map;
|
|
73496
|
+
const file = rerunLogPath(cwd);
|
|
73497
|
+
if (!file)
|
|
73498
|
+
return counts;
|
|
73499
|
+
let text;
|
|
73500
|
+
try {
|
|
73501
|
+
text = fs16.readFileSync(file, "utf8");
|
|
73502
|
+
} catch {
|
|
73503
|
+
return counts;
|
|
73504
|
+
}
|
|
73505
|
+
for (const raw of text.split(`
|
|
73506
|
+
`)) {
|
|
73507
|
+
if (!raw.trim())
|
|
73508
|
+
continue;
|
|
73509
|
+
let row;
|
|
73510
|
+
try {
|
|
73511
|
+
row = JSON.parse(raw);
|
|
73512
|
+
} catch {
|
|
73513
|
+
continue;
|
|
73514
|
+
}
|
|
73515
|
+
if (isRow(row) && row.repo === key.repo && row.pr === key.pr && row.head === key.head)
|
|
73516
|
+
counts.set(row.check, (counts.get(row.check) ?? 0) + 1);
|
|
73517
|
+
}
|
|
73518
|
+
return counts;
|
|
73519
|
+
}
|
|
73520
|
+
function recordReruns(cwd, rows) {
|
|
73521
|
+
if (rows.length === 0)
|
|
73522
|
+
return true;
|
|
73523
|
+
const file = rerunLogPath(cwd);
|
|
73524
|
+
if (!file)
|
|
73525
|
+
return false;
|
|
73526
|
+
try {
|
|
73527
|
+
fs16.mkdirSync(path35.dirname(file), { recursive: true });
|
|
73528
|
+
fs16.appendFileSync(file, rows.map((row) => `${JSON.stringify(row)}
|
|
73529
|
+
`).join(""));
|
|
73530
|
+
return true;
|
|
73531
|
+
} catch {
|
|
73532
|
+
return false;
|
|
73533
|
+
}
|
|
73534
|
+
}
|
|
73535
|
+
function withRerunLock(cwd, fn) {
|
|
73536
|
+
const file = rerunLogPath(cwd);
|
|
73537
|
+
if (!file)
|
|
73538
|
+
return fn();
|
|
73539
|
+
const lock = `${file.slice(0, -".jsonl".length)}.lock`;
|
|
73540
|
+
fs16.mkdirSync(path35.dirname(lock), { recursive: true });
|
|
73541
|
+
const until = Date.now() + LOCK_WAIT_MS;
|
|
73542
|
+
for (;; ) {
|
|
73543
|
+
try {
|
|
73544
|
+
fs16.closeSync(fs16.openSync(lock, "wx"));
|
|
73545
|
+
break;
|
|
73546
|
+
} catch (error) {
|
|
73547
|
+
if (error.code !== "EEXIST")
|
|
73548
|
+
throw error;
|
|
73549
|
+
try {
|
|
73550
|
+
if (Date.now() - fs16.statSync(lock).mtimeMs > LOCK_STALE_MS) {
|
|
73551
|
+
fs16.rmSync(lock, { force: true });
|
|
73552
|
+
continue;
|
|
73553
|
+
}
|
|
73554
|
+
} catch {
|
|
73555
|
+
continue;
|
|
73556
|
+
}
|
|
73557
|
+
if (Date.now() >= until)
|
|
73558
|
+
return null;
|
|
73559
|
+
pause(50);
|
|
73560
|
+
}
|
|
73561
|
+
}
|
|
73562
|
+
try {
|
|
73563
|
+
return fn();
|
|
73564
|
+
} finally {
|
|
73565
|
+
fs16.rmSync(lock, { force: true });
|
|
73566
|
+
}
|
|
73567
|
+
}
|
|
73568
|
+
var isRow = (value) => {
|
|
73569
|
+
const row = value;
|
|
73570
|
+
return !!row && row.type === "ci.rerun" && typeof row.pr === "number" && typeof row.head === "string" && typeof row.check === "string" && typeof row.repo === "string";
|
|
73571
|
+
}, LOCK_WAIT_MS = 2000, LOCK_STALE_MS = 60000, pause = (ms) => {
|
|
73572
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
73573
|
+
};
|
|
73574
|
+
var init_reruns = __esm(() => {
|
|
73575
|
+
init_rev();
|
|
73576
|
+
});
|
|
73577
|
+
|
|
73578
|
+
// packages/workit-core/src/forge/types.ts
|
|
73579
|
+
var success2 = (data) => ({ ok: true, data }), failure3 = (code, error, unblock) => ({ ok: false, code, error, ...unblock ? { unblock } : {} });
|
|
73580
|
+
|
|
73581
|
+
// packages/workit-core/src/forge/report.ts
|
|
73582
|
+
function gatingChecks(status) {
|
|
73583
|
+
const required = status.checks.filter((check) => check.required === true);
|
|
73584
|
+
const protectionNamesChecks = (status.requiredContexts?.length ?? 0) > 0;
|
|
73585
|
+
return required.length > 0 || protectionNamesChecks ? { checks: required, protected: true } : { checks: status.checks, protected: false };
|
|
73586
|
+
}
|
|
73587
|
+
function missingRequired(status) {
|
|
73588
|
+
if (status.requiredContexts === null)
|
|
73589
|
+
return null;
|
|
73590
|
+
const seen = new Set(status.checks.map((check) => check.context));
|
|
73591
|
+
return status.requiredContexts.filter((context) => !seen.has(context));
|
|
73592
|
+
}
|
|
73593
|
+
function checksState(checks, missing = []) {
|
|
73594
|
+
if (checks.some((check) => check.state === "failing"))
|
|
73595
|
+
return "failing";
|
|
73596
|
+
if (checks.some((check) => check.state === "pending") || (missing?.length ?? 0) > 0)
|
|
73597
|
+
return "pending";
|
|
73598
|
+
return checks.length > 0 ? "passing" : "none";
|
|
73599
|
+
}
|
|
73600
|
+
function blockersOf(input) {
|
|
73601
|
+
if (input.state === "merged")
|
|
73602
|
+
return [{ next: "MERGED", reason: "merged" }];
|
|
73603
|
+
if (input.state === "closed")
|
|
73604
|
+
return [{ next: "CLOSED", reason: "closed" }];
|
|
73605
|
+
const out = [];
|
|
73606
|
+
const ms = input.mergeState ?? "";
|
|
73607
|
+
if (input.conflicts)
|
|
73608
|
+
out.push({ next: "RESOLVE_CONFLICTS", reason: "conflicts" });
|
|
73609
|
+
if (input.rebaseRequired)
|
|
73610
|
+
out.push({ next: "REBASE", reason: "behind_base_required" });
|
|
73611
|
+
if (input.threads > 0 || ms === "discussions_not_resolved")
|
|
73612
|
+
out.push({ next: "RESOLVE_THREADS", reason: "unresolved_threads" });
|
|
73613
|
+
if (input.checks === "failing")
|
|
73614
|
+
out.push({
|
|
73615
|
+
next: "FIX_CI",
|
|
73616
|
+
reason: input.checksRequired ? "required_checks_failing" : "checks_failing"
|
|
73617
|
+
});
|
|
73618
|
+
else if (input.checks === "pending" || ms === "ci_still_running" || ms === "ci_must_pass" && input.checks !== "passing")
|
|
73619
|
+
out.push({ next: "WAITING_CI", reason: "checks_pending" });
|
|
73620
|
+
if (input.reviewDecision === "changes_requested" || ms === "requested_changes")
|
|
73621
|
+
out.push({ next: "ADDRESS_REVIEW", reason: "changes_requested" });
|
|
73622
|
+
else if (input.reviewDecision === "review_required" || ms === "not_approved")
|
|
73623
|
+
out.push({ next: "REVIEW", reason: "review_required" });
|
|
73624
|
+
if (input.draft || ms === "draft" || ms === "draft_status")
|
|
73625
|
+
out.push({ next: "MARK_READY", reason: "draft" });
|
|
73626
|
+
if (input.inMergeQueue)
|
|
73627
|
+
out.push({ next: "IN_MERGE_QUEUE", reason: "in_merge_queue" });
|
|
73628
|
+
if (out.length === 0) {
|
|
73629
|
+
const allowed = input.forge === "github" ? GITHUB_MERGEABLE.has(ms) : GITLAB_MERGEABLE.has(ms);
|
|
73630
|
+
if (input.mergeable === "unknown")
|
|
73631
|
+
out.push({ next: "NOT_MERGEABLE", reason: "mergeability_unknown" });
|
|
73632
|
+
else if (!allowed)
|
|
73633
|
+
out.push({ next: "NOT_MERGEABLE", reason: `merge_state_${ms || "unknown"}` });
|
|
73634
|
+
}
|
|
73635
|
+
return out;
|
|
73636
|
+
}
|
|
73637
|
+
function computeBehindBase(cwd, remotes, baseSha, headSha_, options = {}) {
|
|
73638
|
+
const empty = { behind: null, ahead: null, upToDate: null };
|
|
73639
|
+
if (!baseSha)
|
|
73640
|
+
return success2({ ...empty, baseSha: null, error: "the forge reported no base tip" });
|
|
73641
|
+
for (const [sha, remote] of [
|
|
73642
|
+
[baseSha, remotes.base],
|
|
73643
|
+
[headSha_, remotes.head]
|
|
73644
|
+
]) {
|
|
73645
|
+
if (hasCommit(cwd, sha))
|
|
73646
|
+
continue;
|
|
73647
|
+
const fetched = fetchRefs(cwd, remote, [sha], options);
|
|
73648
|
+
if (!fetched.ok && remote !== remotes.base)
|
|
73649
|
+
fetchRefs(cwd, remotes.base, [sha], options);
|
|
73650
|
+
if (!hasCommit(cwd, sha))
|
|
73651
|
+
return success2({
|
|
73652
|
+
...empty,
|
|
73653
|
+
baseSha,
|
|
73654
|
+
error: fetched.ok ? `${sha.slice(0, 12)} is not available locally` : fetched.error
|
|
73655
|
+
});
|
|
73656
|
+
}
|
|
73657
|
+
const counts = aheadBehind(cwd, baseSha, headSha_);
|
|
73658
|
+
return success2(counts ? { ...counts, baseSha } : { ...empty, baseSha, error: "ahead/behind did not resolve" });
|
|
73659
|
+
}
|
|
73660
|
+
function selectPr(cwd, resolved, options) {
|
|
73661
|
+
if (options.pr)
|
|
73662
|
+
return success2(options.pr);
|
|
73663
|
+
const branch = options.branch ?? currentBranch2(cwd);
|
|
73664
|
+
if (!branch)
|
|
73665
|
+
return failure3("invalid_input", "HEAD is detached; pass --pr <n> or --branch <name>");
|
|
73666
|
+
const localSha = resolveRef(cwd, `refs/heads/${branch}`);
|
|
73667
|
+
const found = resolved.forge.findPr(branch, {
|
|
73668
|
+
owner: resolved.headRepo.split("/")[0] ?? null,
|
|
73669
|
+
projectId: resolved.headProjectId,
|
|
73670
|
+
sha: localSha
|
|
73671
|
+
});
|
|
73672
|
+
if (!found.ok)
|
|
73673
|
+
return found;
|
|
73674
|
+
if (!found.data) {
|
|
73675
|
+
const noun = resolved.forge.kind === "github" ? "pull request" : "merge request";
|
|
73676
|
+
return failure3("not_found", `no ${noun} for branch ${branch} from ${resolved.headRepo} in ${resolved.forge.repo}`, "push the branch and open one (workit pr create, S11), or pass --pr <n>");
|
|
73677
|
+
}
|
|
73678
|
+
return success2(found.data.number);
|
|
73679
|
+
}
|
|
73680
|
+
function buildStatusDoc(cwd, resolved, status, options) {
|
|
73681
|
+
const { forge } = resolved;
|
|
73682
|
+
const reruns = rerunCounts(cwd, { repo: forge.repo, pr: status.number, head: status.head.sha });
|
|
73683
|
+
const gating = gatingChecks(status);
|
|
73684
|
+
const missing = missingRequired(status);
|
|
73685
|
+
const failing = status.checks.filter((check) => check.state === "failing");
|
|
73686
|
+
const failingDocs = failing.map((check, index) => {
|
|
73687
|
+
const doc = {
|
|
73688
|
+
name: check.name,
|
|
73689
|
+
url: check.url,
|
|
73690
|
+
runId: check.runId,
|
|
73691
|
+
jobId: check.jobId,
|
|
73692
|
+
conclusion: check.conclusion,
|
|
73693
|
+
required: check.required,
|
|
73694
|
+
logTail: [],
|
|
73695
|
+
rerunsOnHead: reruns.get(check.name) ?? 0
|
|
73696
|
+
};
|
|
73697
|
+
if (check.jobId !== null && options.logLines > 0 && index < MAX_LOG_TAILS) {
|
|
73698
|
+
const tail = forge.jobLogTail({ jobId: check.jobId, scope: check.scope }, options.logLines);
|
|
73699
|
+
if (tail.ok)
|
|
73700
|
+
doc.logTail = tail.data;
|
|
73701
|
+
else
|
|
73702
|
+
doc.logError = tail.error;
|
|
73703
|
+
}
|
|
73704
|
+
return doc;
|
|
73705
|
+
});
|
|
73706
|
+
const local = currentBranch2(cwd) === status.head.branch ? headSha(cwd) : resolveRef(cwd, `refs/heads/${status.head.branch}`);
|
|
73707
|
+
let behindBase = null;
|
|
73708
|
+
if (options.behind && status.state === "open") {
|
|
73709
|
+
const counted = computeBehindBase(cwd, { base: resolved.baseRemote, head: resolved.remote }, status.baseSha, status.head.sha, options.behindTimeoutMs === undefined ? {} : { timeoutMs: options.behindTimeoutMs });
|
|
73710
|
+
if (!counted.ok)
|
|
73711
|
+
return counted;
|
|
73712
|
+
behindBase = counted.data;
|
|
73713
|
+
}
|
|
73714
|
+
const state = checksState(gating.checks, missing);
|
|
73715
|
+
const blockers = blockersOf({
|
|
73716
|
+
forge: forge.kind,
|
|
73717
|
+
state: status.state,
|
|
73718
|
+
draft: status.draft,
|
|
73719
|
+
conflicts: status.conflicts,
|
|
73720
|
+
rebaseRequired: status.rebaseRequired,
|
|
73721
|
+
mergeable: status.mergeable,
|
|
73722
|
+
mergeState: status.mergeState,
|
|
73723
|
+
inMergeQueue: status.inMergeQueue,
|
|
73724
|
+
reviewDecision: status.reviewDecision,
|
|
73725
|
+
threads: status.threads.length,
|
|
73726
|
+
checks: state,
|
|
73727
|
+
checksRequired: gating.protected
|
|
73728
|
+
});
|
|
73729
|
+
return success2({
|
|
73730
|
+
forge: forge.kind,
|
|
73731
|
+
repo: forge.repo,
|
|
73732
|
+
headRepo: resolved.fork ? resolved.headRepo : null,
|
|
73733
|
+
number: status.number,
|
|
73734
|
+
url: status.url,
|
|
73735
|
+
state: status.state,
|
|
73736
|
+
draft: status.draft,
|
|
73737
|
+
base: status.base,
|
|
73738
|
+
head: {
|
|
73739
|
+
branch: status.head.branch,
|
|
73740
|
+
sha: status.head.sha,
|
|
73741
|
+
localSha: local,
|
|
73742
|
+
pushed: local === null ? null : local === status.head.sha
|
|
73743
|
+
},
|
|
73744
|
+
mergeable: status.mergeable,
|
|
73745
|
+
mergeState: status.mergeState,
|
|
73746
|
+
conflicts: status.conflicts,
|
|
73747
|
+
rebaseRequired: status.rebaseRequired,
|
|
73748
|
+
inMergeQueue: status.inMergeQueue,
|
|
73749
|
+
behindBase,
|
|
73750
|
+
checks: {
|
|
73751
|
+
state,
|
|
73752
|
+
failing: failingDocs,
|
|
73753
|
+
pending: status.checks.filter((check) => check.state === "pending").map((check) => check.name),
|
|
73754
|
+
passing: status.checks.filter((check) => check.state === "passing").length,
|
|
73755
|
+
missingRequired: missing
|
|
73756
|
+
},
|
|
73757
|
+
reviews: { decision: status.reviewDecision, unresolvedThreads: status.threads },
|
|
73758
|
+
blockers: status.state === "open" ? blockers.map((blocker) => blocker.reason) : [],
|
|
73759
|
+
identity: options.identity ?? null,
|
|
73760
|
+
truncated: status.truncated,
|
|
73761
|
+
next: blockers[0]?.next ?? "READY"
|
|
73762
|
+
});
|
|
73763
|
+
}
|
|
73764
|
+
function prStatusReport(cwd, resolved, options = {}) {
|
|
73765
|
+
const number = selectPr(cwd, resolved, options);
|
|
73766
|
+
if (!number.ok)
|
|
73767
|
+
return number;
|
|
73768
|
+
const status = resolved.forge.prStatus(number.data);
|
|
73769
|
+
if (!status.ok)
|
|
73770
|
+
return status;
|
|
73771
|
+
const doc = buildStatusDoc(cwd, resolved, status.data, {
|
|
73772
|
+
logLines: options.logLines ?? 60,
|
|
73773
|
+
behind: options.behind ?? true,
|
|
73774
|
+
identity: options.identity
|
|
73775
|
+
});
|
|
73776
|
+
if (!doc.ok)
|
|
73777
|
+
return doc;
|
|
73778
|
+
return success2({ doc: doc.data, raw: status.data });
|
|
73779
|
+
}
|
|
73780
|
+
function waitVerdict(doc, options) {
|
|
73781
|
+
if (doc.state !== "open")
|
|
73782
|
+
return {
|
|
73783
|
+
state: "blocked",
|
|
73784
|
+
reason: `pr_${doc.state}`,
|
|
73785
|
+
unblock: `the ${doc.forge === "github" ? "PR" : "MR"} is ${doc.state}; nothing to wait for`
|
|
73786
|
+
};
|
|
73787
|
+
if (options.head && !doc.head.sha.startsWith(options.head))
|
|
73788
|
+
return { state: "waiting", reason: "head_mismatch" };
|
|
73789
|
+
const checks = doc.checks.state;
|
|
73790
|
+
if (checks === "failing")
|
|
73791
|
+
return { state: "failed", reason: "checks_failing" };
|
|
73792
|
+
if (checks === "pending")
|
|
73793
|
+
return { state: "waiting", reason: "checks_pending" };
|
|
73794
|
+
if (checks === "passing")
|
|
73795
|
+
return { state: "ready", reason: "checks_passing" };
|
|
73796
|
+
if (doc.conflicts)
|
|
73797
|
+
return {
|
|
73798
|
+
state: "blocked",
|
|
73799
|
+
reason: "conflicts",
|
|
73800
|
+
unblock: `rebase onto ${doc.base} and push; CI does not run on a conflicting PR`
|
|
73801
|
+
};
|
|
73802
|
+
const ciRequired = doc.checks.missingRequired === null || doc.mergeState === "ci_must_pass" || doc.mergeState === "ci_still_running";
|
|
73803
|
+
if (ciRequired)
|
|
73804
|
+
return { state: "waiting", reason: "no_checks_yet" };
|
|
73805
|
+
if (options.elapsedMs < (options.noChecksGraceMs ?? NO_CHECKS_GRACE_MS))
|
|
73806
|
+
return { state: "waiting", reason: "no_checks_yet" };
|
|
73807
|
+
return { state: "ready", reason: "no_checks" };
|
|
73808
|
+
}
|
|
73809
|
+
function pollDelay(intervalMs, poll) {
|
|
73810
|
+
const cap = Math.max(intervalMs, 120000);
|
|
73811
|
+
return Math.min(cap, Math.round(intervalMs * 1.5 ** poll));
|
|
73812
|
+
}
|
|
73813
|
+
function planRerun(status, selection) {
|
|
73814
|
+
let checks;
|
|
73815
|
+
if (selection.names.length > 0) {
|
|
73816
|
+
const unknown = selection.names.filter((name) => !status.checks.some((check) => check.name === name));
|
|
73817
|
+
if (unknown.length)
|
|
73818
|
+
return failure3("invalid_input", `unknown check ${unknown.map((name) => JSON.stringify(name)).join(", ")}; known: ${status.checks.map((check) => check.name).join(", ") || "none"}`);
|
|
73819
|
+
checks = status.checks.filter((check) => selection.names.includes(check.name));
|
|
73820
|
+
const running = checks.filter((check) => check.state === "pending");
|
|
73821
|
+
if (running.length)
|
|
73822
|
+
return failure3("busy", `still running: ${running.map((check) => check.name).join(", ")}`, "workit ci wait");
|
|
73823
|
+
} else {
|
|
73824
|
+
checks = status.checks.filter((check) => check.state === "failing");
|
|
73825
|
+
}
|
|
73826
|
+
if (checks.length === 0)
|
|
73827
|
+
return failure3("not_found", `no failing checks on head ${status.head.sha.slice(0, 12)}`);
|
|
73828
|
+
const rerunnable = checks.filter((check) => check.jobId !== null && check.runId !== null);
|
|
73829
|
+
const skipped = checks.filter((check) => !rerunnable.includes(check)).map((check) => check.name);
|
|
73830
|
+
const targets = [];
|
|
73831
|
+
if (selection.names.length === 0) {
|
|
73832
|
+
const busy = status.checks.filter((check) => check.state === "pending" && check.runId !== null && rerunnable.some((failing) => failing.runId === check.runId));
|
|
73833
|
+
if (busy.length)
|
|
73834
|
+
return failure3("busy", `the failed jobs' run is still in progress (${busy.map((check) => check.name).join(", ")})`, "workit ci wait, then rerun");
|
|
73835
|
+
const seen = new Set;
|
|
73836
|
+
for (const check of rerunnable)
|
|
73837
|
+
if (!seen.has(check.runId)) {
|
|
73838
|
+
seen.add(check.runId);
|
|
73839
|
+
targets.push({ kind: "failed_in_run", runId: check.runId, scope: check.scope });
|
|
73840
|
+
}
|
|
73841
|
+
} else {
|
|
73842
|
+
for (const check of rerunnable)
|
|
73843
|
+
targets.push({ kind: "job", jobId: check.jobId, scope: check.scope });
|
|
73844
|
+
}
|
|
73845
|
+
if (targets.length === 0)
|
|
73846
|
+
return failure3("unavailable", `${skipped.join(", ")} cannot be rerun from here (not a CI job); rerun it on the forge`);
|
|
73847
|
+
return success2({ checks: rerunnable, targets, skipped });
|
|
73848
|
+
}
|
|
73849
|
+
function executeRerun(cwd, resolved, status, options) {
|
|
73850
|
+
if (status.state !== "open")
|
|
73851
|
+
return failure3("blocked", `${status.state} PR: CI cannot be rerun`, "nothing to do");
|
|
73852
|
+
const plan = planRerun(status, options);
|
|
73853
|
+
if (!plan.ok)
|
|
73854
|
+
return plan;
|
|
73855
|
+
const locked = withRerunLock(cwd, () => rerunLocked(cwd, resolved, status, plan.data, options));
|
|
73856
|
+
return locked ?? failure3("busy", "another ci rerun is in progress for this repository", "retry in a moment");
|
|
73857
|
+
}
|
|
73858
|
+
function rerunLocked(cwd, resolved, status, plan, options) {
|
|
73859
|
+
const { forge } = resolved;
|
|
73860
|
+
const counts = rerunCounts(cwd, { repo: forge.repo, pr: status.number, head: status.head.sha });
|
|
73861
|
+
const repeated = plan.checks.filter((check) => (counts.get(check.name) ?? 0) > 0);
|
|
73862
|
+
if (repeated.length && !options.force)
|
|
73863
|
+
return failure3("blocked", `rerun_limit: ${repeated.map((check) => check.name).join(", ")} already rerun once on head ${status.head.sha.slice(0, 12)}`, "treat it as a real failure and fix it; if it is a confirmed flake, rerun with --force");
|
|
73864
|
+
const accepted = [];
|
|
73865
|
+
for (const target of plan.targets) {
|
|
73866
|
+
const done = forge.rerun(target);
|
|
73867
|
+
if (!done.ok) {
|
|
73868
|
+
recordAccepted(cwd, resolved, status, accepted, options);
|
|
73869
|
+
return failure3(done.code, accepted.length ? `${done.error} (already rerun: ${accepted.map((check) => check.name).join(", ")})` : done.error, done.unblock);
|
|
73870
|
+
}
|
|
73871
|
+
accepted.push(...plan.checks.filter((check) => target.kind === "failed_in_run" ? check.runId === target.runId : check.jobId === target.jobId));
|
|
73872
|
+
}
|
|
73873
|
+
const recorded = recordAccepted(cwd, resolved, status, accepted, options);
|
|
73874
|
+
return success2({
|
|
73875
|
+
pr: status.number,
|
|
73876
|
+
head: status.head.sha,
|
|
73877
|
+
reason: options.reason,
|
|
73878
|
+
forced: options.force,
|
|
73879
|
+
rerun: accepted.map((check) => ({ name: check.name, runId: check.runId, jobId: check.jobId })),
|
|
73880
|
+
skipped: plan.skipped,
|
|
73881
|
+
recorded
|
|
73882
|
+
});
|
|
73883
|
+
}
|
|
73884
|
+
function recordAccepted(cwd, resolved, status, accepted, options) {
|
|
73885
|
+
const at = new Date().toISOString();
|
|
73886
|
+
const rows = accepted.map((check) => ({
|
|
73887
|
+
v: 1,
|
|
73888
|
+
at,
|
|
73889
|
+
type: "ci.rerun",
|
|
73890
|
+
forge: resolved.forge.kind,
|
|
73891
|
+
repo: resolved.forge.repo,
|
|
73892
|
+
pr: status.number,
|
|
73893
|
+
head: status.head.sha,
|
|
73894
|
+
check: check.name,
|
|
73895
|
+
reason: options.reason,
|
|
73896
|
+
forced: options.force
|
|
73897
|
+
}));
|
|
73898
|
+
return recordReruns(cwd, rows);
|
|
73899
|
+
}
|
|
73900
|
+
var MAX_LOG_TAILS = 5, GITLAB_MERGEABLE, GITHUB_MERGEABLE, NO_CHECKS_GRACE_MS = 90000;
|
|
73901
|
+
var init_report = __esm(() => {
|
|
73902
|
+
init_rev();
|
|
73903
|
+
init_reruns();
|
|
73904
|
+
GITLAB_MERGEABLE = new Set(["mergeable"]);
|
|
73905
|
+
GITHUB_MERGEABLE = new Set(["clean", "unstable", "has_hooks"]);
|
|
73906
|
+
});
|
|
73907
|
+
|
|
73908
|
+
// packages/workit-core/src/forge/redact.ts
|
|
73909
|
+
function redactText(value) {
|
|
73910
|
+
let out = stripAnsi2(value).replace(PRIVATE_KEY, "[REDACTED PRIVATE KEY]");
|
|
73911
|
+
out = out.replace(WEBHOOK, `$1/${R}`);
|
|
73912
|
+
out = out.replace(URL_USERINFO, `$1${R}@`);
|
|
73913
|
+
for (const pattern of TOKEN_PATTERNS)
|
|
73914
|
+
out = out.replace(pattern, R);
|
|
73915
|
+
out = out.replace(CURL_USER, `$1$2${R}$2`);
|
|
73916
|
+
out = out.replace(BEARER, `$1${R}`);
|
|
73917
|
+
out = out.replace(QUOTED_ASSIGNMENT, (match, lead, quote, secret) => secret.startsWith(R) ? match : `${lead}${quote}${R}${quote}`);
|
|
73918
|
+
out = out.replace(NETRC, `$1${R}`);
|
|
73919
|
+
for (const pattern of FLAG_SECRETS)
|
|
73920
|
+
out = out.replace(pattern, `$1${R}`);
|
|
73921
|
+
out = out.replace(QUERY_SECRET, `$1${R}`);
|
|
73922
|
+
out = out.replace(ASSIGNMENT, (match, lead, scheme, secret) => secret.startsWith(R) ? match : `${lead}${scheme ?? ""}${R}`);
|
|
73923
|
+
return out.split(`
|
|
73924
|
+
`).map((line) => line.replace(BASE64, (blob) => /\bbase64\b/iu.test(line) || decodesToSecret(blob) ? R : blob)).join(`
|
|
73925
|
+
`);
|
|
73926
|
+
}
|
|
73927
|
+
function logTail(text, lines) {
|
|
73928
|
+
if (lines <= 0)
|
|
73929
|
+
return [];
|
|
73930
|
+
const cleaned = text.replace(GITLAB_SECTION, "").split(/\r?\n/u).map((raw) => stripAnsi2(raw).split("\r").findLast((part) => part.trim()) ?? "").map((raw) => raw.replace(LINE_NOISE, ""));
|
|
73931
|
+
const all = redactText(cleaned.join(`
|
|
73932
|
+
`)).split(`
|
|
73933
|
+
`);
|
|
73934
|
+
let end = all.length;
|
|
73935
|
+
for (let index = all.length - 1;index >= 0; index -= 1)
|
|
73936
|
+
if (FAILURE_END.some((pattern) => pattern.test(all[index]))) {
|
|
73937
|
+
end = index + 1;
|
|
73938
|
+
break;
|
|
73939
|
+
}
|
|
73940
|
+
const kept = all.slice(0, end);
|
|
73941
|
+
while (kept.length > 0 && !kept[kept.length - 1].trim())
|
|
73942
|
+
kept.pop();
|
|
73943
|
+
const cap = (value) => value.length > MAX_LINE ? `${value.slice(0, MAX_LINE)}…` : value;
|
|
73944
|
+
const tailStart = Math.max(0, kept.length - lines);
|
|
73945
|
+
const find = (pattern) => kept.findIndex((value) => pattern.test(value) && !NOT_FIRST_ERROR.test(value));
|
|
73946
|
+
const strong = find(STRONG_ERROR);
|
|
73947
|
+
const first = strong >= 0 ? strong : find(WEAK_ERROR);
|
|
73948
|
+
if (first < 0 || first >= tailStart || lines < 8)
|
|
73949
|
+
return kept.slice(tailStart).map(cap);
|
|
73950
|
+
const headCount = Math.ceil((lines - 1) * (2 / 3));
|
|
73951
|
+
const from = Math.max(0, first - LEAD);
|
|
73952
|
+
const head = kept.slice(from, from + headCount);
|
|
73953
|
+
const tailCount = lines - 1 - head.length;
|
|
73954
|
+
const tailFrom = Math.max(from + head.length, kept.length - tailCount);
|
|
73955
|
+
const tail = kept.slice(tailFrom);
|
|
73956
|
+
return [...head, ...tailFrom > from + head.length ? ["…"] : [], ...tail].map(cap);
|
|
73957
|
+
}
|
|
73958
|
+
function shortBody(value, max = 300) {
|
|
73959
|
+
const flat = redactText(value).replace(/\s+/gu, " ").trim();
|
|
73960
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
|
|
73961
|
+
}
|
|
73962
|
+
var R = "[REDACTED]", TOKEN_PATTERNS, WEBHOOK, URL_USERINFO, CURL_USER, KEY, ASSIGNMENT, BEARER, QUOTED_ASSIGNMENT, NETRC, FLAG_SECRETS, QUERY_SECRET, PRIVATE_KEY, BASE64, ANSI, stripAnsi2 = (value) => value.replace(ANSI, ""), decodesToSecret = (blob) => {
|
|
73963
|
+
if (/^[0-9a-f]+$/iu.test(blob))
|
|
73964
|
+
return false;
|
|
73965
|
+
let decoded;
|
|
73966
|
+
try {
|
|
73967
|
+
decoded = Buffer.from(blob, "base64").toString("utf8");
|
|
73968
|
+
} catch {
|
|
73969
|
+
return false;
|
|
73970
|
+
}
|
|
73971
|
+
if (!/^[\x20-\x7e\s]+$/u.test(decoded))
|
|
73972
|
+
return false;
|
|
73973
|
+
return TOKEN_PATTERNS.some((pattern) => new RegExp(pattern.source, "u").test(decoded)) || /^[^\s:]{1,64}:[^\s]{6,}$/u.test(decoded.trim());
|
|
73974
|
+
}, LINE_NOISE, GITLAB_SECTION, FAILURE_END, MAX_LINE = 400, STRONG_ERROR, WEAK_ERROR, NOT_FIRST_ERROR, LEAD = 3;
|
|
73975
|
+
var init_redact = __esm(() => {
|
|
73976
|
+
TOKEN_PATTERNS = [
|
|
73977
|
+
/\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})/gu,
|
|
73978
|
+
/\bgl[a-z]{1,8}-[A-Za-z0-9_-]{16,}/gu,
|
|
73979
|
+
/\bnpm_[A-Za-z0-9]{30,}/gu,
|
|
73980
|
+
/\bxox[abposr]-[A-Za-z0-9-]{10,}/gu,
|
|
73981
|
+
/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/gu,
|
|
73982
|
+
/\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{20,}/gu,
|
|
73983
|
+
/\b[sr]k_(?:live|test)_[A-Za-z0-9]{16,}/gu,
|
|
73984
|
+
/\bAIza[0-9A-Za-z_-]{35}/gu,
|
|
73985
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/gu
|
|
73986
|
+
];
|
|
73987
|
+
WEBHOOK = /\b(https?:\/\/(?:hooks\.slack\.com\/(?:services|workflows|triggers)|discord(?:app)?\.com\/api\/webhooks))\/[^\s"'<>]+/giu;
|
|
73988
|
+
URL_USERINFO = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/giu;
|
|
73989
|
+
CURL_USER = /(\s(?:-u|--user)(?:\s+|=))(["']?)[^\s"':]+:[^\s"']+\2/gu;
|
|
73990
|
+
KEY = String.raw`[A-Za-z0-9_.-]*?(?:token|secret|password|passwd|pwd|api[_-]?key|access[_-]?key|account[_-]?key|private[_-]?key|client[_-]?secret|credentials?|authorization|cookie|signature)[A-Za-z0-9_.-]*`;
|
|
73991
|
+
ASSIGNMENT = new RegExp(String.raw`(?<![A-Za-z0-9])(["']?${KEY}["']?\s*[:=]\s*["']?)((?:bearer|basic|token|digest)\s+)?([^\s"',;&]+)`, "giu");
|
|
73992
|
+
BEARER = /\b(bearer\s+)[A-Za-z0-9._~+/-]{12,}=*/giu;
|
|
73993
|
+
QUOTED_ASSIGNMENT = new RegExp(String.raw`(?<![A-Za-z0-9])(["']?${KEY}["']?\s*[:=]\s*)(["'])([^"'\n]*[^\s"'][^"'\n]*)\2`, "giu");
|
|
73994
|
+
NETRC = /\b((?:machine|login)\s+\S+\s+(?:login\s+\S+\s+)?password\s+)\S+/giu;
|
|
73995
|
+
FLAG_SECRETS = [
|
|
73996
|
+
/\b(sshpass\s+-p\s*)\S+/gu,
|
|
73997
|
+
/\b(mysql(?:dump|admin|import|show)?\b[^\n]*?\s-p)(?!\s|$)\S+/gu
|
|
73998
|
+
];
|
|
73999
|
+
QUERY_SECRET = /([?&;](?:sig|signature|token|access_token|api_key|apikey|key|password|secret|client_secret|X-Amz-Signature|X-Goog-Signature)=)[^&\s#"']+/giu;
|
|
74000
|
+
PRIVATE_KEY = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----|$)/gu;
|
|
74001
|
+
BASE64 = /[A-Za-z0-9+/]{32,}={0,2}/gu;
|
|
74002
|
+
ANSI = new RegExp(String.raw`\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b\][^\u0007]*\u0007`, "gu");
|
|
74003
|
+
LINE_NOISE = /^(?:\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\s?)?(?:\d{2}[A-Z]\s)?/u;
|
|
74004
|
+
GITLAB_SECTION = /section_(?:start|end):\d+:[A-Za-z0-9_.-]+(?:\[[^\]]*\])?\r?/gu;
|
|
74005
|
+
FAILURE_END = [/##\[error\]Process completed with exit code/u, /^ERROR: Job failed\b/u];
|
|
74006
|
+
STRONG_ERROR = /^\s*(?:\(fail\)|FAIL\b|✗|✕|×|error(?:\[[A-Z0-9]+\])?:|[A-Za-z]*Error\b:|AssertionError|Traceback|panic:|Exception\b)/u;
|
|
74007
|
+
WEAK_ERROR = /\berror\b[:[]|\bERROR\b|\bFAIL(?:ED)?\b|\bfailed\b|\bassert/u;
|
|
74008
|
+
NOT_FIRST_ERROR = /^\s*(?:\(pass\)|\(skip\)|\(todo\)|✓|✔|ok\b|PASS\b)|^\s*\d+ fail\b|##\[error\]Process completed|^\s*0 (?:errors?|fail)/u;
|
|
74009
|
+
});
|
|
74010
|
+
|
|
74011
|
+
// packages/workit-core/src/forge/exec.ts
|
|
74012
|
+
import { spawnSync as spawnSync14 } from "node:child_process";
|
|
74013
|
+
import fs17 from "node:fs";
|
|
74014
|
+
import path36 from "node:path";
|
|
74015
|
+
function findExecutable(bin, env) {
|
|
74016
|
+
const dirs = (env.PATH ?? env.Path ?? "").split(path36.delimiter).filter(Boolean);
|
|
74017
|
+
const exts = process.platform === "win32" ? [".exe"] : [""];
|
|
74018
|
+
for (const dir of dirs)
|
|
74019
|
+
for (const ext of exts) {
|
|
74020
|
+
const candidate = path36.join(dir, `${bin}${ext}`);
|
|
74021
|
+
try {
|
|
74022
|
+
const stat = fs17.statSync(candidate);
|
|
74023
|
+
if (stat.isFile())
|
|
74024
|
+
return candidate;
|
|
74025
|
+
} catch {}
|
|
74026
|
+
}
|
|
74027
|
+
return null;
|
|
74028
|
+
}
|
|
74029
|
+
function systemRunner(kind, apiHost, env = process.env) {
|
|
74030
|
+
const childEnv = {
|
|
74031
|
+
...env,
|
|
74032
|
+
...kind === "github" ? { GH_HOST: apiHost } : { GITLAB_HOST: apiHost },
|
|
74033
|
+
GH_PROMPT_DISABLED: "1",
|
|
74034
|
+
GH_NO_UPDATE_NOTIFIER: "1",
|
|
74035
|
+
GLAB_CHECK_UPDATE: "false",
|
|
74036
|
+
NO_PROMPT: "1",
|
|
74037
|
+
NO_COLOR: "1",
|
|
74038
|
+
GH_PAGER: "",
|
|
74039
|
+
GLAB_PAGER: "",
|
|
74040
|
+
PAGER: ""
|
|
74041
|
+
};
|
|
74042
|
+
return (bin, args, options) => {
|
|
74043
|
+
const exe = findExecutable(bin, env);
|
|
74044
|
+
if (!exe)
|
|
74045
|
+
return { status: null, stdout: "", stderr: "", timedOut: false, missing: true };
|
|
74046
|
+
const token = options.token ? kind === "github" ? { GH_TOKEN: options.token, GH_ENTERPRISE_TOKEN: options.token } : { GITLAB_TOKEN: options.token } : {};
|
|
74047
|
+
const result = spawnSync14(exe, [...args], {
|
|
74048
|
+
encoding: "utf8",
|
|
74049
|
+
env: { ...childEnv, ...token },
|
|
74050
|
+
maxBuffer: MAX_BUFFER2,
|
|
74051
|
+
timeout: options.timeoutMs,
|
|
74052
|
+
killSignal: "SIGKILL",
|
|
74053
|
+
windowsHide: true,
|
|
74054
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
74055
|
+
});
|
|
74056
|
+
const error = result.error;
|
|
74057
|
+
return {
|
|
74058
|
+
status: result.status,
|
|
74059
|
+
stdout: result.stdout ?? "",
|
|
74060
|
+
stderr: result.stderr ?? "",
|
|
74061
|
+
timedOut: error?.code === "ETIMEDOUT",
|
|
74062
|
+
missing: error?.code === "ENOENT"
|
|
74063
|
+
};
|
|
74064
|
+
};
|
|
74065
|
+
}
|
|
74066
|
+
function cliFailure(bin, apiHost, run, what, timeoutMs) {
|
|
74067
|
+
if (run.missing)
|
|
74068
|
+
return failure3("unavailable", `${bin} is not installed`, installHint(bin));
|
|
74069
|
+
if (run.timedOut)
|
|
74070
|
+
return failure3("unavailable", `${bin} api ${what} timed out after ${timeoutMs} ms`);
|
|
74071
|
+
const stderr = redactText(run.stderr).trim();
|
|
74072
|
+
if (/auth login|not logged in|no token|authentication required|bad credentials|\b401\b|unauthorized/iu.test(stderr))
|
|
74073
|
+
return failure3("unavailable", `${bin} is not authenticated for ${apiHost}`, loginHint(bin, apiHost));
|
|
74074
|
+
if (/\b404\b|not found/iu.test(stderr))
|
|
74075
|
+
return failure3("not_found", `${what} was not found`);
|
|
74076
|
+
const first = stderr.split(/\r?\n/u).find((value) => value.trim()) ?? `exit ${run.status}`;
|
|
74077
|
+
return failure3("failed", `${bin} api ${what} failed: ${first.slice(0, 200)}`);
|
|
74078
|
+
}
|
|
74079
|
+
function apiJson(runner, bin, apiHost, args, what, timeoutMs = FORGE_TIMEOUTS.api, options = {}) {
|
|
74080
|
+
const run = runner(bin, args, { timeoutMs });
|
|
74081
|
+
if (run.status !== 0) {
|
|
74082
|
+
if (options.graphql && !run.missing && !run.timedOut)
|
|
74083
|
+
try {
|
|
74084
|
+
const body = JSON.parse(run.stdout);
|
|
74085
|
+
if (Array.isArray(body.errors))
|
|
74086
|
+
return { ok: true, data: body };
|
|
74087
|
+
} catch {}
|
|
74088
|
+
return cliFailure(bin, apiHost, run, what, timeoutMs);
|
|
74089
|
+
}
|
|
74090
|
+
try {
|
|
74091
|
+
return { ok: true, data: JSON.parse(run.stdout) };
|
|
74092
|
+
} catch {
|
|
74093
|
+
return failure3("failed", `${bin} api ${what} returned invalid JSON`);
|
|
74094
|
+
}
|
|
74095
|
+
}
|
|
74096
|
+
function apiText(runner, bin, apiHost, args, what, timeoutMs = FORGE_TIMEOUTS.log) {
|
|
74097
|
+
const run = runner(bin, args, { timeoutMs });
|
|
74098
|
+
if (run.status !== 0)
|
|
74099
|
+
return cliFailure(bin, apiHost, run, what, timeoutMs);
|
|
74100
|
+
return { ok: true, data: run.stdout };
|
|
74101
|
+
}
|
|
74102
|
+
var FORGE_TIMEOUTS, MAX_BUFFER2, installHint = (bin) => bin === "gh" ? "install the GitHub CLI (https://cli.github.com) and run: gh auth login" : "install glab (https://gitlab.com/gitlab-org/cli) and run: glab auth login", loginHint = (bin, apiHost) => `${bin} auth login --hostname ${apiHost}`;
|
|
74103
|
+
var init_exec = __esm(() => {
|
|
74104
|
+
init_redact();
|
|
74105
|
+
FORGE_TIMEOUTS = { api: 20000, log: 30000 };
|
|
74106
|
+
MAX_BUFFER2 = 64 * 1024 * 1024;
|
|
74107
|
+
});
|
|
74108
|
+
|
|
74109
|
+
// packages/workit-core/src/forge/github.ts
|
|
74110
|
+
function checkFromContext(node) {
|
|
74111
|
+
const required = node.isRequired ?? null;
|
|
74112
|
+
if (node.__typename === "StatusContext") {
|
|
74113
|
+
const state = node.state.toUpperCase();
|
|
74114
|
+
return {
|
|
74115
|
+
name: node.context,
|
|
74116
|
+
context: node.context,
|
|
74117
|
+
state: state === "SUCCESS" ? "passing" : state === "FAILURE" || state === "ERROR" ? "failing" : "pending",
|
|
74118
|
+
conclusion: state.toLowerCase(),
|
|
74119
|
+
url: safeUrl(node.targetUrl),
|
|
74120
|
+
runId: null,
|
|
74121
|
+
jobId: null,
|
|
74122
|
+
scope: null,
|
|
74123
|
+
required
|
|
74124
|
+
};
|
|
74125
|
+
}
|
|
74126
|
+
if (node.conclusion === "STALE")
|
|
74127
|
+
return null;
|
|
74128
|
+
const run = node.checkSuite?.workflowRun ?? null;
|
|
74129
|
+
const workflow = run?.workflow?.name;
|
|
74130
|
+
const completed = node.status === "COMPLETED";
|
|
74131
|
+
const conclusion = node.conclusion?.toUpperCase() ?? null;
|
|
74132
|
+
return {
|
|
74133
|
+
name: workflow ? `${workflow} / ${node.name}` : node.name,
|
|
74134
|
+
context: node.name,
|
|
74135
|
+
state: !completed ? "pending" : conclusion && FAILING.has(conclusion) ? "failing" : conclusion === "SKIPPED" || conclusion === "NEUTRAL" ? "skipped" : "passing",
|
|
74136
|
+
conclusion: completed ? conclusion?.toLowerCase() ?? null : node.status.toLowerCase(),
|
|
74137
|
+
url: safeUrl(node.detailsUrl),
|
|
74138
|
+
runId: run?.databaseId ?? null,
|
|
74139
|
+
jobId: run ? node.databaseId ?? null : null,
|
|
74140
|
+
scope: null,
|
|
74141
|
+
required
|
|
74142
|
+
};
|
|
74143
|
+
}
|
|
74144
|
+
function latestPerName(checks) {
|
|
74145
|
+
const byName = new Map;
|
|
74146
|
+
for (const check of checks) {
|
|
74147
|
+
const seen = byName.get(check.name);
|
|
74148
|
+
if (!seen || (check.jobId ?? 0) >= (seen.jobId ?? 0))
|
|
74149
|
+
byName.set(check.name, check);
|
|
74150
|
+
}
|
|
74151
|
+
return [...byName.values()];
|
|
74152
|
+
}
|
|
74153
|
+
function threadFrom(node) {
|
|
74154
|
+
const first = node.comments.nodes[0];
|
|
74155
|
+
return {
|
|
74156
|
+
id: node.id,
|
|
74157
|
+
path: node.path,
|
|
74158
|
+
line: node.line,
|
|
74159
|
+
author: first?.author?.login ?? null,
|
|
74160
|
+
isBot: first?.author?.__typename === "Bot",
|
|
74161
|
+
outdated: node.isOutdated,
|
|
74162
|
+
url: safeUrl(first?.url ?? null),
|
|
74163
|
+
body: shortBody(first?.body ?? "")
|
|
74164
|
+
};
|
|
74165
|
+
}
|
|
74166
|
+
function createGitHubForge(options) {
|
|
74167
|
+
const { apiHost, repo, runner } = options;
|
|
74168
|
+
const maxPages = options.maxPages ?? GITHUB_MAX_PAGES;
|
|
74169
|
+
const [owner, name] = repo.split("/");
|
|
74170
|
+
const rest = (endpoint, what) => apiJson(runner, "gh", apiHost, ["api", endpoint], what);
|
|
74171
|
+
const graphql = (query, variables, what) => {
|
|
74172
|
+
const args = ["api", "graphql", "-f", `query=${query}`];
|
|
74173
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
74174
|
+
if (value === null)
|
|
74175
|
+
continue;
|
|
74176
|
+
args.push(typeof value === "number" ? "-F" : "-f", `${key}=${value}`);
|
|
74177
|
+
}
|
|
74178
|
+
const response = apiJson(runner, "gh", apiHost, args, what, FORGE_TIMEOUTS.api, { graphql: true });
|
|
74179
|
+
if (!response.ok)
|
|
74180
|
+
return response;
|
|
74181
|
+
if (response.data.errors?.length) {
|
|
74182
|
+
const message = redactText(response.data.errors[0]?.message ?? "GraphQL error").slice(0, 300);
|
|
74183
|
+
return /could not resolve to a (pullrequest|repository)/iu.test(message) ? failure3("not_found", `${what}: ${message}`) : failure3("failed", `${what}: ${message}`);
|
|
74184
|
+
}
|
|
74185
|
+
if (!response.data.data)
|
|
74186
|
+
return failure3("failed", `${what}: empty GraphQL response`);
|
|
74187
|
+
return success2(response.data.data);
|
|
74188
|
+
};
|
|
74189
|
+
const fetchPr = (number, cursors) => {
|
|
74190
|
+
const data = graphql(PR_STATUS_QUERY, { owner, name, number, threads: cursors.threads, contexts: cursors.contexts }, `pull request #${number}`);
|
|
74191
|
+
if (!data.ok)
|
|
74192
|
+
return data;
|
|
74193
|
+
const pr = data.data.repository?.pullRequest;
|
|
74194
|
+
return pr ? success2(pr) : failure3("not_found", `pull request #${number} was not found`);
|
|
74195
|
+
};
|
|
74196
|
+
const requiredContexts = (base) => {
|
|
74197
|
+
const branch = rest(`repos/${repo}/branches/${encodeRef(base)}`, `branch ${base}`);
|
|
74198
|
+
const rules = rest(`repos/${repo}/rules/branches/${encodeRef(base)}`, `rules for ${base}`);
|
|
74199
|
+
if (!branch.ok || !rules.ok || !Array.isArray(rules.data))
|
|
74200
|
+
return null;
|
|
74201
|
+
const contexts = new Set;
|
|
74202
|
+
const classic = branch.data.protection?.required_status_checks;
|
|
74203
|
+
for (const context of classic?.contexts ?? [])
|
|
74204
|
+
contexts.add(context);
|
|
74205
|
+
for (const check of classic?.checks ?? [])
|
|
74206
|
+
if (check.context)
|
|
74207
|
+
contexts.add(check.context);
|
|
74208
|
+
for (const rule of rules.data)
|
|
74209
|
+
if (rule.type === "required_status_checks") {
|
|
74210
|
+
const listed = rule.parameters?.required_status_checks;
|
|
74211
|
+
if (Array.isArray(listed)) {
|
|
74212
|
+
for (const item of listed)
|
|
74213
|
+
if (typeof item.context === "string")
|
|
74214
|
+
contexts.add(item.context);
|
|
74215
|
+
}
|
|
74216
|
+
}
|
|
74217
|
+
return [...contexts];
|
|
74218
|
+
};
|
|
74219
|
+
return {
|
|
74220
|
+
kind: "github",
|
|
74221
|
+
apiHost,
|
|
74222
|
+
repo,
|
|
74223
|
+
identity(expected) {
|
|
74224
|
+
const run = runner("gh", ["api", "user"], { timeoutMs: FORGE_TIMEOUTS.api });
|
|
74225
|
+
if (run.status !== 0) {
|
|
74226
|
+
if (/HTTP 403|resource not accessible by integration/iu.test(run.stderr))
|
|
74227
|
+
return success2({
|
|
74228
|
+
login: null,
|
|
74229
|
+
expected,
|
|
74230
|
+
matches: null,
|
|
74231
|
+
note: "the credential cannot read /user (app or Actions token); identity not checked"
|
|
74232
|
+
});
|
|
74233
|
+
return cliFailure("gh", apiHost, run, "user", FORGE_TIMEOUTS.api);
|
|
74234
|
+
}
|
|
74235
|
+
let login = "";
|
|
74236
|
+
try {
|
|
74237
|
+
const user = JSON.parse(run.stdout);
|
|
74238
|
+
login = typeof user.login === "string" ? user.login : "";
|
|
74239
|
+
} catch {}
|
|
74240
|
+
if (!login)
|
|
74241
|
+
return failure3("failed", "gh api user returned no login");
|
|
74242
|
+
return success2({
|
|
74243
|
+
login,
|
|
74244
|
+
expected,
|
|
74245
|
+
matches: expected ? login.toLowerCase() === expected.toLowerCase() : null
|
|
74246
|
+
});
|
|
74247
|
+
},
|
|
74248
|
+
repoInfo(target) {
|
|
74249
|
+
const info = rest(`repos/${target}`, `repository ${target}`);
|
|
74250
|
+
if (!info.ok)
|
|
74251
|
+
return info;
|
|
74252
|
+
return success2({
|
|
74253
|
+
id: typeof info.data.id === "number" ? info.data.id : null,
|
|
74254
|
+
parent: info.data.fork && info.data.parent?.full_name ? info.data.parent.full_name : null
|
|
74255
|
+
});
|
|
74256
|
+
},
|
|
74257
|
+
findPr(head, match) {
|
|
74258
|
+
const data = graphql(FIND_PR_QUERY, { owner, name, head }, `pull requests for ${head}`);
|
|
74259
|
+
if (!data.ok)
|
|
74260
|
+
return data;
|
|
74261
|
+
const nodes = data.data.repository?.pullRequests.nodes ?? [];
|
|
74262
|
+
const ours = nodes.filter((node) => node.headRefName === head && (match.owner !== null && node.headRepositoryOwner?.login.toLowerCase() === match.owner.toLowerCase() || match.sha !== null && node.headRefOid === match.sha));
|
|
74263
|
+
const pick = ours.find((node) => node.state === "OPEN") ?? ours[0];
|
|
74264
|
+
if (!pick)
|
|
74265
|
+
return success2(null);
|
|
74266
|
+
const ref = {
|
|
74267
|
+
number: pick.number,
|
|
74268
|
+
url: pick.url,
|
|
74269
|
+
state: prState(pick.state),
|
|
74270
|
+
headBranch: pick.headRefName,
|
|
74271
|
+
headSha: pick.headRefOid
|
|
74272
|
+
};
|
|
74273
|
+
return success2(ref);
|
|
74274
|
+
},
|
|
74275
|
+
prStatus(number) {
|
|
74276
|
+
const first = fetchPr(number, { threads: null, contexts: null });
|
|
74277
|
+
if (!first.ok)
|
|
74278
|
+
return first;
|
|
74279
|
+
const pr = first.data;
|
|
74280
|
+
const threads = [...pr.reviewThreads.nodes];
|
|
74281
|
+
const rollup = pr.commits.nodes[0]?.commit.statusCheckRollup ?? null;
|
|
74282
|
+
const contexts = [...rollup?.contexts.nodes ?? []];
|
|
74283
|
+
let threadPage = pr.reviewThreads.pageInfo;
|
|
74284
|
+
let contextPage = rollup?.contexts.pageInfo ?? { hasNextPage: false, endCursor: null };
|
|
74285
|
+
let pages = 1;
|
|
74286
|
+
while ((threadPage.hasNextPage || contextPage.hasNextPage) && pages < maxPages) {
|
|
74287
|
+
pages += 1;
|
|
74288
|
+
const next = fetchPr(number, {
|
|
74289
|
+
threads: threadPage.hasNextPage ? threadPage.endCursor : null,
|
|
74290
|
+
contexts: contextPage.hasNextPage ? contextPage.endCursor : null
|
|
74291
|
+
});
|
|
74292
|
+
if (!next.ok)
|
|
74293
|
+
return next;
|
|
74294
|
+
if (next.data.headRefOid !== pr.headRefOid)
|
|
74295
|
+
return failure3("failed", `pull request #${number} head moved while paging; retry`);
|
|
74296
|
+
if (threadPage.hasNextPage) {
|
|
74297
|
+
threads.push(...next.data.reviewThreads.nodes);
|
|
74298
|
+
threadPage = next.data.reviewThreads.pageInfo;
|
|
74299
|
+
}
|
|
74300
|
+
const nextRollup = next.data.commits.nodes[0]?.commit.statusCheckRollup;
|
|
74301
|
+
if (contextPage.hasNextPage && nextRollup) {
|
|
74302
|
+
contexts.push(...nextRollup.contexts.nodes);
|
|
74303
|
+
contextPage = nextRollup.contexts.pageInfo;
|
|
74304
|
+
}
|
|
74305
|
+
}
|
|
74306
|
+
const checks = latestPerName(contexts.map(checkFromContext).filter((check) => check !== null));
|
|
74307
|
+
const open = pr.state === "OPEN";
|
|
74308
|
+
const status = {
|
|
74309
|
+
number: pr.number,
|
|
74310
|
+
url: pr.url,
|
|
74311
|
+
state: prState(pr.state),
|
|
74312
|
+
draft: pr.isDraft,
|
|
74313
|
+
base: pr.baseRefName,
|
|
74314
|
+
baseSha: pr.baseRef?.target?.oid ?? pr.baseRefOid,
|
|
74315
|
+
head: { branch: pr.headRefName, sha: pr.headRefOid },
|
|
74316
|
+
mergeable: pr.mergeable === "MERGEABLE" ? "yes" : pr.mergeable === "CONFLICTING" ? "no" : "unknown",
|
|
74317
|
+
conflicts: pr.mergeable === "CONFLICTING" || pr.mergeStateStatus === "DIRTY",
|
|
74318
|
+
rebaseRequired: pr.mergeStateStatus === "BEHIND",
|
|
74319
|
+
reviewDecision: pr.reviewDecision?.toLowerCase() ?? null,
|
|
74320
|
+
mergeState: pr.mergeStateStatus?.toLowerCase() ?? null,
|
|
74321
|
+
inMergeQueue: pr.isInMergeQueue === true,
|
|
74322
|
+
checks,
|
|
74323
|
+
requiredContexts: open ? requiredContexts(pr.baseRefName) : null,
|
|
74324
|
+
threads: threads.filter((node) => !node.isResolved).map(threadFrom),
|
|
74325
|
+
truncated: threadPage.hasNextPage || contextPage.hasNextPage
|
|
74326
|
+
};
|
|
74327
|
+
return success2(status);
|
|
74328
|
+
},
|
|
74329
|
+
jobLogTail(job, lines) {
|
|
74330
|
+
const text = apiText(runner, "gh", apiHost, ["api", `repos/${repo}/actions/jobs/${job.jobId}/logs`], `job ${job.jobId} log`, FORGE_TIMEOUTS.log);
|
|
74331
|
+
return text.ok ? success2(logTail(text.data, lines)) : text;
|
|
74332
|
+
},
|
|
74333
|
+
rerun(target) {
|
|
74334
|
+
const endpoint = target.kind === "failed_in_run" ? `repos/${repo}/actions/runs/${target.runId}/rerun-failed-jobs` : `repos/${repo}/actions/jobs/${target.jobId}/rerun`;
|
|
74335
|
+
const what = target.kind === "failed_in_run" ? `rerun of run ${target.runId}` : `rerun of job ${target.jobId}`;
|
|
74336
|
+
const run = apiText(runner, "gh", apiHost, ["api", "-X", "POST", endpoint], what, FORGE_TIMEOUTS.api);
|
|
74337
|
+
return run.ok ? success2(undefined) : run;
|
|
74338
|
+
}
|
|
74339
|
+
};
|
|
74340
|
+
}
|
|
74341
|
+
var GITHUB_MAX_PAGES = 10, PR_FIELDS = `number url state isDraft isInMergeQueue mergeable mergeStateStatus baseRefName baseRefOid baseRef { target { oid } } headRefName headRefOid reviewDecision`, THREADS = `reviewThreads(first: 100, after: $threads) { pageInfo { hasNextPage endCursor } nodes { id isResolved isOutdated path line comments(first: 1) { nodes { author { login __typename } body url } } } }`, CONTEXTS = `contexts(first: 100, after: $contexts) { pageInfo { hasNextPage endCursor } nodes { __typename ... on CheckRun { name status conclusion detailsUrl databaseId isRequired(pullRequestNumber: $number) checkSuite { workflowRun { databaseId workflow { name } } } } ... on StatusContext { context state targetUrl isRequired(pullRequestNumber: $number) } } }`, PR_STATUS_QUERY, FIND_PR_QUERY = `query($owner: String!, $name: String!, $head: String!) { repository(owner: $owner, name: $name) { pullRequests(headRefName: $head, first: 20, orderBy: { field: CREATED_AT, direction: DESC }) { nodes { number url state headRefName headRefOid headRepositoryOwner { login } } } } }`, prState = (value) => value === "MERGED" ? "merged" : value === "CLOSED" ? "closed" : "open", FAILING, safeUrl = (value) => value ? redactText(value) : null, encodeRef = (ref) => ref.split("/").map(encodeURIComponent).join("/");
|
|
74342
|
+
var init_github = __esm(() => {
|
|
74343
|
+
init_exec();
|
|
74344
|
+
init_redact();
|
|
74345
|
+
PR_STATUS_QUERY = `query($owner: String!, $name: String!, $number: Int!, $threads: String, $contexts: String) { repository(owner: $owner, name: $name) { pullRequest(number: $number) { ${PR_FIELDS} ${THREADS} commits(last: 1) { nodes { commit { oid statusCheckRollup { state ${CONTEXTS} } } } } } } }`;
|
|
74346
|
+
FAILING = new Set([
|
|
74347
|
+
"FAILURE",
|
|
74348
|
+
"TIMED_OUT",
|
|
74349
|
+
"CANCELLED",
|
|
74350
|
+
"ACTION_REQUIRED",
|
|
74351
|
+
"STARTUP_FAILURE"
|
|
74352
|
+
]);
|
|
74353
|
+
});
|
|
74354
|
+
|
|
74355
|
+
// packages/workit-core/src/forge/gitlab.ts
|
|
74356
|
+
function checkFromBridge(bridge, pipelineId, scope) {
|
|
74357
|
+
const status = bridge.downstream_pipeline?.status ?? bridge.status;
|
|
74358
|
+
const allow = bridge.allow_failure === true;
|
|
74359
|
+
const failed = status === "failed" || status === "canceled";
|
|
74360
|
+
return {
|
|
74361
|
+
name: `${bridge.stage} / ${bridge.name}`,
|
|
74362
|
+
context: bridge.name,
|
|
74363
|
+
state: stateOf(status, allow),
|
|
74364
|
+
conclusion: failed && allow ? "allowed_failure" : `downstream_${status}`,
|
|
74365
|
+
url: safeUrl2(bridge.downstream_pipeline?.web_url ?? bridge.web_url ?? null),
|
|
74366
|
+
runId: pipelineId,
|
|
74367
|
+
jobId: null,
|
|
74368
|
+
scope,
|
|
74369
|
+
required: !allow
|
|
74370
|
+
};
|
|
74371
|
+
}
|
|
74372
|
+
function checkFromJob(job, pipelineId, scope) {
|
|
74373
|
+
const failed = job.status === "failed" || job.status === "canceled";
|
|
74374
|
+
return {
|
|
74375
|
+
name: `${job.stage} / ${job.name}`,
|
|
74376
|
+
context: job.name,
|
|
74377
|
+
state: stateOf(job.status, job.allow_failure === true),
|
|
74378
|
+
conclusion: failed && job.allow_failure ? "allowed_failure" : job.status,
|
|
74379
|
+
url: safeUrl2(job.web_url ?? null),
|
|
74380
|
+
runId: pipelineId,
|
|
74381
|
+
jobId: job.id,
|
|
74382
|
+
scope,
|
|
74383
|
+
required: job.allow_failure !== true
|
|
74384
|
+
};
|
|
74385
|
+
}
|
|
74386
|
+
function createGitLabForge(options) {
|
|
74387
|
+
const { apiHost, repo, runner } = options;
|
|
74388
|
+
const maxPages = options.maxPages ?? GITLAB_MAX_PAGES;
|
|
74389
|
+
const project = `projects/${encodeURIComponent(repo)}`;
|
|
74390
|
+
const api = (endpoint) => ["api", endpoint];
|
|
74391
|
+
const pages = (endpoint, what) => {
|
|
74392
|
+
const items = [];
|
|
74393
|
+
const sep = endpoint.includes("?") ? "&" : "?";
|
|
74394
|
+
for (let page = 1;page <= maxPages; page += 1) {
|
|
74395
|
+
const chunk = apiJson(runner, "glab", apiHost, api(`${endpoint}${sep}per_page=${PER_PAGE}&page=${page}`), what);
|
|
74396
|
+
if (!chunk.ok)
|
|
74397
|
+
return chunk;
|
|
74398
|
+
if (!Array.isArray(chunk.data))
|
|
74399
|
+
return failure3("failed", `${what}: expected a list`);
|
|
74400
|
+
items.push(...chunk.data);
|
|
74401
|
+
if (chunk.data.length < PER_PAGE)
|
|
74402
|
+
return success2({ items, truncated: false });
|
|
74403
|
+
}
|
|
74404
|
+
return success2({ items, truncated: true });
|
|
74405
|
+
};
|
|
74406
|
+
const mergeCommitTests = (scope, sha, head) => {
|
|
74407
|
+
const commit = apiJson(runner, "glab", apiHost, api(`projects/${scope}/repository/commits/${sha}`), `commit ${sha.slice(0, 12)}`);
|
|
74408
|
+
return commit.ok && Array.isArray(commit.data.parent_ids) && commit.data.parent_ids.includes(head);
|
|
74409
|
+
};
|
|
74410
|
+
const baseTip = (branch) => {
|
|
74411
|
+
const tip = apiJson(runner, "glab", apiHost, api(`${project}/repository/branches/${encodeURIComponent(branch)}`), `branch ${branch}`);
|
|
74412
|
+
return tip.ok && typeof tip.data.commit?.id === "string" ? tip.data.commit.id : null;
|
|
74413
|
+
};
|
|
74414
|
+
return {
|
|
74415
|
+
kind: "gitlab",
|
|
74416
|
+
apiHost,
|
|
74417
|
+
repo,
|
|
74418
|
+
identity(expected) {
|
|
74419
|
+
const user = apiJson(runner, "glab", apiHost, api("user"), "user");
|
|
74420
|
+
if (!user.ok && user.code === "failed" && /\b403\b|forbidden/iu.test(user.error))
|
|
74421
|
+
return success2({
|
|
74422
|
+
login: null,
|
|
74423
|
+
expected,
|
|
74424
|
+
matches: null,
|
|
74425
|
+
note: "the credential cannot read /user (job or project token); identity not checked"
|
|
74426
|
+
});
|
|
74427
|
+
if (!user.ok)
|
|
74428
|
+
return user;
|
|
74429
|
+
const login = typeof user.data.username === "string" ? user.data.username : "";
|
|
74430
|
+
if (!login)
|
|
74431
|
+
return failure3("failed", "glab api user returned no username");
|
|
74432
|
+
return success2({
|
|
74433
|
+
login,
|
|
74434
|
+
expected,
|
|
74435
|
+
matches: expected ? login.toLowerCase() === expected.toLowerCase() : null
|
|
74436
|
+
});
|
|
74437
|
+
},
|
|
74438
|
+
repoInfo(target) {
|
|
74439
|
+
const info = apiJson(runner, "glab", apiHost, api(`projects/${encodeURIComponent(target)}`), `project ${target}`);
|
|
74440
|
+
if (!info.ok)
|
|
74441
|
+
return info;
|
|
74442
|
+
return success2({
|
|
74443
|
+
id: typeof info.data.id === "number" ? info.data.id : null,
|
|
74444
|
+
parent: info.data.forked_from_project?.path_with_namespace ?? null
|
|
74445
|
+
});
|
|
74446
|
+
},
|
|
74447
|
+
findPr(head, match) {
|
|
74448
|
+
const list = apiJson(runner, "glab", apiHost, api(`${project}/merge_requests?source_branch=${encodeURIComponent(head)}&order_by=created_at&sort=desc&per_page=20`), `merge requests for ${head}`);
|
|
74449
|
+
if (!list.ok)
|
|
74450
|
+
return list;
|
|
74451
|
+
if (!Array.isArray(list.data))
|
|
74452
|
+
return failure3("failed", "merge request list is not a list");
|
|
74453
|
+
const matches = list.data.filter((mr) => mr.source_branch === head && (match.projectId !== null && mr.source_project_id === match.projectId || match.sha !== null && mr.sha === match.sha));
|
|
74454
|
+
const pick = matches.find((mr) => mr.state === "opened") ?? matches[0];
|
|
74455
|
+
return success2(pick ? {
|
|
74456
|
+
number: pick.iid,
|
|
74457
|
+
url: pick.web_url,
|
|
74458
|
+
state: mrState(pick.state),
|
|
74459
|
+
headBranch: pick.source_branch,
|
|
74460
|
+
headSha: pick.sha ?? null
|
|
74461
|
+
} : null);
|
|
74462
|
+
},
|
|
74463
|
+
prStatus(iid) {
|
|
74464
|
+
const mr = apiJson(runner, "glab", apiHost, api(`${project}/merge_requests/${iid}`), `merge request !${iid}`);
|
|
74465
|
+
if (!mr.ok)
|
|
74466
|
+
return mr;
|
|
74467
|
+
const value = mr.data;
|
|
74468
|
+
let truncated = false;
|
|
74469
|
+
let checks = [];
|
|
74470
|
+
const pipeline = value.head_pipeline ?? null;
|
|
74471
|
+
const open = value.state === "opened";
|
|
74472
|
+
const current = pipeline !== null && (pipeline.sha === value.sha || pipeline.source_sha === value.sha || !pipeline.source_sha && new RegExp(`^refs/merge-requests/${value.iid}/(?:merge|train)$`, "u").test(pipeline.ref ?? "") && mergeCommitTests(pipeline.project_id, pipeline.sha, value.sha));
|
|
74473
|
+
if (pipeline && current) {
|
|
74474
|
+
const scope = String(pipeline.project_id);
|
|
74475
|
+
const jobs = pages(`projects/${scope}/pipelines/${pipeline.id}/jobs`, `pipeline ${pipeline.id} jobs`);
|
|
74476
|
+
if (!jobs.ok)
|
|
74477
|
+
return jobs;
|
|
74478
|
+
const bridges = pages(`projects/${scope}/pipelines/${pipeline.id}/bridges`, `pipeline ${pipeline.id} bridges`);
|
|
74479
|
+
if (!bridges.ok)
|
|
74480
|
+
return bridges;
|
|
74481
|
+
truncated ||= jobs.data.truncated || bridges.data.truncated;
|
|
74482
|
+
checks = [
|
|
74483
|
+
...jobs.data.items.map((job) => checkFromJob(job, pipeline.id, scope)),
|
|
74484
|
+
...bridges.data.items.map((bridge) => checkFromBridge(bridge, pipeline.id, scope))
|
|
74485
|
+
];
|
|
74486
|
+
const pipelineState = stateOf(pipeline.status, false);
|
|
74487
|
+
if (pipelineState === "failing" && !checks.some((check) => check.state === "failing"))
|
|
74488
|
+
checks.push(pipelineCheck("failing", pipeline.status, pipeline));
|
|
74489
|
+
if (pipelineState === "pending" && !checks.some((check) => check.state === "pending"))
|
|
74490
|
+
checks.push(pipelineCheck("pending", pipeline.status, pipeline));
|
|
74491
|
+
} else if (open && pipeline) {
|
|
74492
|
+
checks = [pipelineCheck("pending", "awaiting_pipeline_for_head", null)];
|
|
74493
|
+
}
|
|
74494
|
+
const discussions = pages(`${project}/merge_requests/${iid}/discussions`, `merge request !${iid} discussions`);
|
|
74495
|
+
if (!discussions.ok)
|
|
74496
|
+
return discussions;
|
|
74497
|
+
truncated ||= discussions.data.truncated;
|
|
74498
|
+
const threads = discussions.data.items.filter((discussion) => discussion.notes.some((note) => note.resolvable === true && note.resolved !== true)).map((discussion) => {
|
|
74499
|
+
const note = discussion.notes[0];
|
|
74500
|
+
return {
|
|
74501
|
+
id: discussion.id,
|
|
74502
|
+
path: note.position?.new_path ?? note.position?.old_path ?? null,
|
|
74503
|
+
line: note.position?.new_line ?? null,
|
|
74504
|
+
author: note.author?.username ?? null,
|
|
74505
|
+
isBot: isBot(note.author),
|
|
74506
|
+
outdated: false,
|
|
74507
|
+
url: safeUrl2(`${value.web_url}#note_${note.id}`),
|
|
74508
|
+
body: shortBody(note.body ?? "")
|
|
74509
|
+
};
|
|
74510
|
+
});
|
|
74511
|
+
const detailed = value.detailed_merge_status ?? "unchecked";
|
|
74512
|
+
const conflicts = value.has_conflicts === true || detailed === "conflict";
|
|
74513
|
+
const status = {
|
|
74514
|
+
number: value.iid,
|
|
74515
|
+
url: value.web_url,
|
|
74516
|
+
state: mrState(value.state),
|
|
74517
|
+
draft: value.draft ?? value.work_in_progress ?? false,
|
|
74518
|
+
base: value.target_branch,
|
|
74519
|
+
baseSha: open ? baseTip(value.target_branch) : null,
|
|
74520
|
+
head: { branch: value.source_branch, sha: value.sha },
|
|
74521
|
+
mergeable: conflicts ? "no" : UNSETTLED.has(detailed) ? "unknown" : "yes",
|
|
74522
|
+
conflicts,
|
|
74523
|
+
rebaseRequired: detailed === "need_rebase",
|
|
74524
|
+
reviewDecision: detailed === "not_approved" ? "review_required" : detailed === "requested_changes" ? "changes_requested" : null,
|
|
74525
|
+
mergeState: detailed,
|
|
74526
|
+
inMergeQueue: false,
|
|
74527
|
+
checks,
|
|
74528
|
+
requiredContexts: [],
|
|
74529
|
+
threads,
|
|
74530
|
+
truncated
|
|
74531
|
+
};
|
|
74532
|
+
return success2(status);
|
|
74533
|
+
},
|
|
74534
|
+
jobLogTail(job, lines) {
|
|
74535
|
+
const scope = job.scope ?? encodeURIComponent(repo);
|
|
74536
|
+
const text = apiText(runner, "glab", apiHost, ["api", `projects/${scope}/jobs/${job.jobId}/trace`], `job ${job.jobId} trace`, FORGE_TIMEOUTS.log);
|
|
74537
|
+
return text.ok ? success2(logTail(text.data, lines)) : text;
|
|
74538
|
+
},
|
|
74539
|
+
rerun(target) {
|
|
74540
|
+
const scope = target.scope ?? encodeURIComponent(repo);
|
|
74541
|
+
const endpoint = target.kind === "failed_in_run" ? `projects/${scope}/pipelines/${target.runId}/retry` : `projects/${scope}/jobs/${target.jobId}/retry`;
|
|
74542
|
+
const what = target.kind === "failed_in_run" ? `retry of pipeline ${target.runId}` : `retry of job ${target.jobId}`;
|
|
74543
|
+
const run = apiText(runner, "glab", apiHost, ["api", "-X", "POST", endpoint], what, FORGE_TIMEOUTS.api);
|
|
74544
|
+
return run.ok ? success2(undefined) : run;
|
|
74545
|
+
}
|
|
74546
|
+
};
|
|
74547
|
+
}
|
|
74548
|
+
var GITLAB_MAX_PAGES = 10, PER_PAGE = 100, mrState = (value) => value === "merged" ? "merged" : value === "opened" ? "open" : "closed", PENDING, stateOf = (status, allowFailure) => {
|
|
74549
|
+
const failed = status === "failed" || status === "canceled";
|
|
74550
|
+
return PENDING.has(status) ? "pending" : failed ? allowFailure ? "passing" : "failing" : status === "success" ? "passing" : "skipped";
|
|
74551
|
+
}, pipelineCheck = (state, conclusion, pipeline) => ({
|
|
74552
|
+
name: "pipeline",
|
|
74553
|
+
context: "pipeline",
|
|
74554
|
+
state,
|
|
74555
|
+
conclusion,
|
|
74556
|
+
url: safeUrl2(pipeline?.web_url ?? null),
|
|
74557
|
+
runId: pipeline?.id ?? null,
|
|
74558
|
+
jobId: null,
|
|
74559
|
+
scope: pipeline ? String(pipeline.project_id) : null,
|
|
74560
|
+
required: true
|
|
74561
|
+
}), safeUrl2 = (value) => value ? redactText(value) : null, isBot = (author) => author?.bot === true || /(?:^project_\d+_bot|bot$|\[bot\]$)/iu.test(author?.username ?? ""), UNSETTLED;
|
|
74562
|
+
var init_gitlab = __esm(() => {
|
|
74563
|
+
init_exec();
|
|
74564
|
+
init_redact();
|
|
74565
|
+
PENDING = new Set([
|
|
74566
|
+
"created",
|
|
74567
|
+
"pending",
|
|
74568
|
+
"running",
|
|
74569
|
+
"waiting_for_resource",
|
|
74570
|
+
"waiting_for_callback",
|
|
74571
|
+
"preparing",
|
|
74572
|
+
"scheduled"
|
|
74573
|
+
]);
|
|
74574
|
+
UNSETTLED = new Set(["checking", "unchecked", "preparing", "approvals_syncing"]);
|
|
74575
|
+
});
|
|
74576
|
+
|
|
74577
|
+
// packages/workit-core/src/forge/resolve.ts
|
|
74578
|
+
import fs18 from "node:fs";
|
|
74579
|
+
function resolveForge(cwd, options = {}) {
|
|
74580
|
+
let workspace;
|
|
74581
|
+
try {
|
|
74582
|
+
workspace = resolveRuntimeWorkspaceVcs(cwd);
|
|
74583
|
+
} catch (error) {
|
|
74584
|
+
return failure3("blocked", error instanceof Error ? error.message : String(error), "fix ~/.config/workit/workspaces.json (workit init → Workspaces)");
|
|
74585
|
+
}
|
|
74586
|
+
const hosts = configuredHosts();
|
|
74587
|
+
const pushed = pushForge(cwd, { branch: options.branch, hosts, sshConfig: options.sshConfig });
|
|
74588
|
+
if (!pushed.ok)
|
|
74589
|
+
return failure3(pushed.code, pushed.error, pushed.unblock);
|
|
74590
|
+
const conflict = forgeConflict(pushed.forge, workspace?.vcs?.provider);
|
|
74591
|
+
if (conflict)
|
|
74592
|
+
return failure3("blocked", conflict.error, conflict.unblock);
|
|
74593
|
+
const derived = pushed.forge;
|
|
74594
|
+
const { kind, apiHost } = derived;
|
|
74595
|
+
const base = options.runner ?? systemRunner(kind, apiHost, options.env ?? process.env);
|
|
74596
|
+
const now = options.now ?? Date.now;
|
|
74597
|
+
const expectedAccount = workspace?.vcs?.account ?? null;
|
|
74598
|
+
let token;
|
|
74599
|
+
let credential = "cli_login";
|
|
74600
|
+
const tokenFile = workspace?.vcs?.tokenFile;
|
|
74601
|
+
if (typeof tokenFile === "string" && tokenFile.trim()) {
|
|
74602
|
+
const workspaceHost = workspace?.vcs?.host;
|
|
74603
|
+
const explicit = typeof workspaceHost === "string" && workspaceHost.trim().toLowerCase().replace(/^https?:\/\//u, "").replace(/\/.*$/u, "") === apiHost;
|
|
74604
|
+
if (derived.via === "host_name" && !explicit)
|
|
74605
|
+
return failure3("blocked", `token_host_unverified: push host ${derived.host} only looks like ${kind}; workspace ${workspace?.name ?? "?"} vcs.tokenFile is not sent to it`, `if ${apiHost} is your ${kind} server, set ${kind}.host in ~/.config/workit/vcs.json (or vcs.host for the workspace); otherwise fix the push remote`);
|
|
74606
|
+
try {
|
|
74607
|
+
token = fs18.readFileSync(tokenFile.trim(), "utf8").trim();
|
|
74608
|
+
} catch {
|
|
74609
|
+
return failure3("blocked", `workspace ${workspace?.name ?? "?"} vcs.tokenFile cannot be read`, "fix vcs.tokenFile in ~/.config/workit/workspaces.json");
|
|
74610
|
+
}
|
|
74611
|
+
if (!token)
|
|
74612
|
+
return failure3("blocked", `workspace ${workspace?.name ?? "?"} vcs.tokenFile is empty`, "fix vcs.tokenFile in ~/.config/workit/workspaces.json");
|
|
74613
|
+
credential = "workspace_token_file";
|
|
74614
|
+
} else if (expectedAccount && kind === "github") {
|
|
74615
|
+
const issued = base("gh", ["auth", "token", "--hostname", apiHost, "--user", expectedAccount], {
|
|
74616
|
+
timeoutMs: FORGE_TIMEOUTS.api
|
|
74617
|
+
});
|
|
74618
|
+
if (issued.missing)
|
|
74619
|
+
return failure3("unavailable", "gh is not installed", "install the GitHub CLI (https://cli.github.com) and run: gh auth login");
|
|
74620
|
+
token = issued.status === 0 ? issued.stdout.trim() : "";
|
|
74621
|
+
if (!token)
|
|
74622
|
+
return failure3("blocked", `identity_unavailable: gh has no login for ${expectedAccount} on ${apiHost} (workspace ${workspace?.name ?? "?"})`, `gh auth login --hostname ${apiHost} # sign in as ${expectedAccount}; workit passes that account's token per call and never switches the active account`);
|
|
74623
|
+
credential = "gh_account_token";
|
|
74624
|
+
}
|
|
74625
|
+
const limits = { deadline: null };
|
|
74626
|
+
const runner = (bin, args, callOptions) => {
|
|
74627
|
+
let timeoutMs = callOptions.timeoutMs;
|
|
74628
|
+
if (limits.deadline !== null) {
|
|
74629
|
+
const remaining = limits.deadline - now();
|
|
74630
|
+
if (remaining <= 0)
|
|
74631
|
+
return { status: null, stdout: "", stderr: "", timedOut: true, missing: false };
|
|
74632
|
+
timeoutMs = Math.min(timeoutMs, remaining);
|
|
74633
|
+
}
|
|
74634
|
+
return base(bin, args, { ...callOptions, timeoutMs, ...token ? { token } : {} });
|
|
74635
|
+
};
|
|
74636
|
+
const headRepo = derived.repo;
|
|
74637
|
+
const probe = createForge(derived, headRepo, runner);
|
|
74638
|
+
const info = probe.repoInfo(headRepo);
|
|
74639
|
+
if (!info.ok && info.code !== "not_found")
|
|
74640
|
+
return info;
|
|
74641
|
+
const headProjectId = info.ok ? info.data.id : null;
|
|
74642
|
+
const remotes = remoteNames(cwd);
|
|
74643
|
+
const repoOfRemote = (name) => {
|
|
74644
|
+
const url = pushUrl(cwd, name);
|
|
74645
|
+
const other = url ? deriveForge(url, { hosts, sshConfig: options.sshConfig }) : null;
|
|
74646
|
+
return other && other.kind === kind && other.apiHost === apiHost ? other.repo : null;
|
|
74647
|
+
};
|
|
74648
|
+
const upstream = pushed.remote !== "upstream" && remotes.includes("upstream") ? repoOfRemote("upstream") : null;
|
|
74649
|
+
const baseRepo = upstream && upstream !== headRepo ? upstream : info.ok && info.data.parent ? info.data.parent : headRepo;
|
|
74650
|
+
const baseRemote = baseRepo === headRepo ? pushed.remote : remotes.find((name) => repoOfRemote(name) === baseRepo) ?? pushed.remote;
|
|
74651
|
+
return success2({
|
|
74652
|
+
forge: baseRepo === headRepo ? probe : createForge(derived, baseRepo, runner),
|
|
74653
|
+
remote: pushed.remote,
|
|
74654
|
+
url: pushed.url,
|
|
74655
|
+
derived,
|
|
74656
|
+
workspace: workspace?.name ?? null,
|
|
74657
|
+
expectedAccount,
|
|
74658
|
+
credential,
|
|
74659
|
+
headRepo,
|
|
74660
|
+
headProjectId,
|
|
74661
|
+
fork: baseRepo !== headRepo,
|
|
74662
|
+
baseRemote,
|
|
74663
|
+
limits
|
|
74664
|
+
});
|
|
74665
|
+
}
|
|
74666
|
+
function checkIdentity(resolved) {
|
|
74667
|
+
const { forge, expectedAccount } = resolved;
|
|
74668
|
+
const identity = forge.identity(expectedAccount);
|
|
74669
|
+
if (!identity.ok)
|
|
74670
|
+
return identity;
|
|
74671
|
+
if (identity.data.matches === false) {
|
|
74672
|
+
const bin = forge.kind === "github" ? "gh" : "glab";
|
|
74673
|
+
const where = `workspace ${resolved.workspace ?? "?"}`;
|
|
74674
|
+
return failure3("blocked", `identity_mismatch: the ${resolved.credential === "workspace_token_file" ? "vcs.tokenFile token" : `${bin} login`} is ${identity.data.login} but ${where} expects ${expectedAccount}`, resolved.credential === "workspace_token_file" ? `put a token for ${expectedAccount} in the ${where} vcs.tokenFile` : `${bin} auth login --hostname ${forge.apiHost} # sign in as ${expectedAccount}`);
|
|
74675
|
+
}
|
|
74676
|
+
return identity;
|
|
74677
|
+
}
|
|
74678
|
+
var configuredHosts = () => {
|
|
74679
|
+
const vcs = readVcsConfig();
|
|
74680
|
+
if (vcs.status !== "valid")
|
|
74681
|
+
return {};
|
|
74682
|
+
const host = (kind) => {
|
|
74683
|
+
const value = vcs.config[kind]?.host;
|
|
74684
|
+
return typeof value === "string" && value.trim() ? [value.trim()] : [];
|
|
74685
|
+
};
|
|
74686
|
+
return { github: host("github"), gitlab: host("gitlab") };
|
|
74687
|
+
}, createForge = (derived, repo, runner) => derived.kind === "github" ? createGitHubForge({ apiHost: derived.apiHost, repo, runner }) : createGitLabForge({ apiHost: derived.apiHost, repo, runner });
|
|
74688
|
+
var init_resolve = __esm(() => {
|
|
74689
|
+
init_vcs_config();
|
|
74690
|
+
init_workspaces();
|
|
74691
|
+
init_rev();
|
|
74692
|
+
init_exec();
|
|
74693
|
+
init_github();
|
|
74694
|
+
init_gitlab();
|
|
74695
|
+
});
|
|
74696
|
+
|
|
74697
|
+
// packages/workit-cli/src/verbs/forge-common.ts
|
|
74698
|
+
function parseFlags(argv, spec) {
|
|
74699
|
+
const parsed = { values: {}, booleans: new Set, lists: {}, positionals: [] };
|
|
74700
|
+
for (let index = 0;index < argv.length; index += 1) {
|
|
74701
|
+
const arg = argv[index];
|
|
74702
|
+
if (arg === "--json")
|
|
74703
|
+
continue;
|
|
74704
|
+
if (!arg.startsWith("--")) {
|
|
74705
|
+
parsed.positionals.push(arg);
|
|
74706
|
+
continue;
|
|
74707
|
+
}
|
|
74708
|
+
const eq = arg.indexOf("=");
|
|
74709
|
+
const name = arg.slice(2, eq < 0 ? undefined : eq);
|
|
74710
|
+
const kind = spec[name];
|
|
74711
|
+
if (!kind)
|
|
74712
|
+
return `unknown option --${name}`;
|
|
74713
|
+
if (kind === "boolean") {
|
|
74714
|
+
if (eq >= 0)
|
|
74715
|
+
return `--${name} takes no value`;
|
|
74716
|
+
parsed.booleans.add(name);
|
|
74717
|
+
continue;
|
|
74718
|
+
}
|
|
74719
|
+
const value = eq >= 0 ? arg.slice(eq + 1) : argv[++index];
|
|
74720
|
+
if (value === undefined || value === "" || eq < 0 && value.startsWith("--"))
|
|
74721
|
+
return `--${name} requires a value`;
|
|
74722
|
+
if (kind === "list")
|
|
74723
|
+
(parsed.lists[name] ??= []).push(value);
|
|
74724
|
+
else
|
|
74725
|
+
parsed.values[name] = value;
|
|
74726
|
+
}
|
|
74727
|
+
return parsed;
|
|
74728
|
+
}
|
|
74729
|
+
function positiveInt(value, flag) {
|
|
74730
|
+
if (value === undefined)
|
|
74731
|
+
return null;
|
|
74732
|
+
return /^[1-9]\d{0,8}$/u.test(value) ? Number(value) : `${flag} must be a positive integer`;
|
|
74733
|
+
}
|
|
74734
|
+
function parseDuration2(value) {
|
|
74735
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/u.exec(value.trim());
|
|
74736
|
+
if (!match)
|
|
74737
|
+
return null;
|
|
74738
|
+
const unit = match[2] ?? "s";
|
|
74739
|
+
const factor = unit === "ms" ? 1 : unit === "s" ? 1000 : unit === "m" ? 60000 : 3600000;
|
|
74740
|
+
return Math.round(Number(match[1]) * factor);
|
|
74741
|
+
}
|
|
74742
|
+
function connect(io, branch) {
|
|
74743
|
+
const resolved = resolveForge(io.cwd, {
|
|
74744
|
+
branch,
|
|
74745
|
+
env: io.env,
|
|
74746
|
+
runner: forgeDeps.runner,
|
|
74747
|
+
now: forgeDeps.now
|
|
74748
|
+
});
|
|
74749
|
+
if (!resolved.ok)
|
|
74750
|
+
return resolved;
|
|
74751
|
+
const identity = checkIdentity(resolved.data);
|
|
74752
|
+
if (!identity.ok)
|
|
74753
|
+
return identity;
|
|
74754
|
+
return {
|
|
74755
|
+
ok: true,
|
|
74756
|
+
data: {
|
|
74757
|
+
...resolved.data,
|
|
74758
|
+
identity: {
|
|
74759
|
+
login: identity.data.login,
|
|
74760
|
+
credential: resolved.data.credential,
|
|
74761
|
+
...identity.data.note ? { note: identity.data.note } : {}
|
|
74762
|
+
}
|
|
74763
|
+
}
|
|
74764
|
+
};
|
|
74765
|
+
}
|
|
74766
|
+
function renderStatus(doc) {
|
|
74767
|
+
const noun = doc.forge === "github" ? `PR #${doc.number}` : `MR !${doc.number}`;
|
|
74768
|
+
const lines = [
|
|
74769
|
+
`${noun} ${doc.state}${doc.draft ? " (draft)" : ""}: ${doc.headRepo ? `${doc.headRepo}:` : ""}${doc.head.branch} -> ${doc.base} ${doc.url}`
|
|
74770
|
+
];
|
|
74771
|
+
const local = doc.head.localSha === null ? "not checked out" : doc.head.pushed ? "local matches" : `local ${short(doc.head.localSha)} differs`;
|
|
74772
|
+
lines.push(`head ${short(doc.head.sha)} (${local})`);
|
|
74773
|
+
const behind = doc.behindBase;
|
|
74774
|
+
const behindText = !behind ? "" : behind.behind === null ? ` · behind ${doc.base}: unknown (${behind.error ?? "unavailable"})` : ` · behind ${doc.base}: ${behind.behind}, ahead ${behind.ahead}`;
|
|
74775
|
+
lines.push(`mergeable ${doc.mergeable} · conflicts ${doc.conflicts ? "yes" : "no"}${doc.rebaseRequired ? " · rebase required" : ""}${behindText}`);
|
|
74776
|
+
const checks = doc.checks;
|
|
74777
|
+
lines.push(`checks ${checks.state}: ${checks.failing.length} failing, ${checks.pending.length} pending, ${checks.passing} passing`);
|
|
74778
|
+
for (const check of checks.failing) {
|
|
74779
|
+
lines.push(` x ${check.name} (${check.conclusion ?? "failed"}${check.required === false ? ", optional" : ""})${check.rerunsOnHead ? ` [rerun ${check.rerunsOnHead}x on this head]` : ""}${check.url ? ` ${check.url}` : ""}`);
|
|
74780
|
+
for (const value of check.logTail)
|
|
74781
|
+
lines.push(` ${value}`);
|
|
74782
|
+
if (check.logError)
|
|
74783
|
+
lines.push(` (log unavailable: ${check.logError})`);
|
|
74784
|
+
}
|
|
74785
|
+
if (checks.pending.length)
|
|
74786
|
+
lines.push(` pending: ${checks.pending.join(", ")}`);
|
|
74787
|
+
if (checks.missingRequired === null)
|
|
74788
|
+
lines.push(" required checks: unknown (branch protection not readable)");
|
|
74789
|
+
else if (checks.missingRequired.length)
|
|
74790
|
+
lines.push(` required, not reported yet: ${checks.missingRequired.join(", ")}`);
|
|
74791
|
+
const threads = doc.reviews.unresolvedThreads;
|
|
74792
|
+
lines.push(`reviews ${doc.reviews.decision ?? "none"} · ${threads.length} unresolved thread${threads.length === 1 ? "" : "s"}`);
|
|
74793
|
+
for (const thread of threads)
|
|
74794
|
+
lines.push(` - ${thread.path ? `${thread.path}${thread.line ? `:${thread.line}` : ""} ` : ""}@${thread.author ?? "?"}${thread.isBot ? " (bot)" : ""}: ${thread.body}`);
|
|
74795
|
+
if (doc.truncated)
|
|
74796
|
+
lines.push("(lists truncated at the page cap)");
|
|
74797
|
+
if (doc.blockers.length)
|
|
74798
|
+
lines.push(`blockers: ${doc.blockers.join(", ")}`);
|
|
74799
|
+
if (doc.identity?.note)
|
|
74800
|
+
lines.push(`identity: ${doc.identity.note}`);
|
|
74801
|
+
lines.push(`next: ${doc.next}`);
|
|
74802
|
+
return lines;
|
|
74803
|
+
}
|
|
74804
|
+
var forgeDeps, usage3 = (io, message, line) => emit(io, fail("invalid_input", message, { unblock: line })), forgeFail = (io, result, data) => emit(io, fail(result.code, result.error, { data, unblock: result.unblock })), short = (sha) => sha ? sha.slice(0, 7) : "?";
|
|
74805
|
+
var init_forge_common = __esm(() => {
|
|
74806
|
+
init_resolve();
|
|
74807
|
+
init_output();
|
|
74808
|
+
forgeDeps = {
|
|
74809
|
+
runner: undefined,
|
|
74810
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
74811
|
+
now: () => Date.now()
|
|
74812
|
+
};
|
|
74813
|
+
});
|
|
74814
|
+
|
|
73019
74815
|
// packages/workit-cli/src/verbs/pr.ts
|
|
73020
74816
|
var exports_pr = {};
|
|
73021
74817
|
__export(exports_pr, {
|
|
73022
74818
|
run: () => run13
|
|
73023
74819
|
});
|
|
73024
|
-
|
|
74820
|
+
async function status(argv, io) {
|
|
74821
|
+
const flags = parseFlags(argv, { pr: "value", branch: "value", "log-lines": "value" });
|
|
74822
|
+
if (typeof flags === "string")
|
|
74823
|
+
return usage3(io, flags, STATUS_USAGE);
|
|
74824
|
+
if (flags.positionals.length)
|
|
74825
|
+
return usage3(io, `unexpected argument ${flags.positionals[0]}`, STATUS_USAGE);
|
|
74826
|
+
const pr = positiveInt(flags.values.pr, "--pr");
|
|
74827
|
+
if (typeof pr === "string")
|
|
74828
|
+
return usage3(io, pr, STATUS_USAGE);
|
|
74829
|
+
if (pr && flags.values.branch)
|
|
74830
|
+
return usage3(io, "pass --pr or --branch, not both", STATUS_USAGE);
|
|
74831
|
+
const logLines = flags.values["log-lines"] === "0" ? 0 : positiveInt(flags.values["log-lines"], "--log-lines");
|
|
74832
|
+
if (typeof logLines === "string")
|
|
74833
|
+
return usage3(io, logLines, STATUS_USAGE);
|
|
74834
|
+
if (logLines !== null && logLines > 500)
|
|
74835
|
+
return usage3(io, "--log-lines is capped at 500", STATUS_USAGE);
|
|
74836
|
+
const connected = connect(io, flags.values.branch);
|
|
74837
|
+
if (!connected.ok)
|
|
74838
|
+
return forgeFail(io, connected);
|
|
74839
|
+
const report = prStatusReport(io.cwd, connected.data, {
|
|
74840
|
+
pr,
|
|
74841
|
+
branch: flags.values.branch ?? null,
|
|
74842
|
+
logLines: logLines ?? 60,
|
|
74843
|
+
identity: connected.data.identity
|
|
74844
|
+
});
|
|
74845
|
+
if (!report.ok)
|
|
74846
|
+
return forgeFail(io, report);
|
|
74847
|
+
return emit(io, ok(report.data.doc), renderStatus);
|
|
74848
|
+
}
|
|
74849
|
+
async function run13(argv, io) {
|
|
74850
|
+
const [sub, ...rest] = argv;
|
|
74851
|
+
if (sub === "status")
|
|
74852
|
+
return status(rest, io);
|
|
74853
|
+
if (sub === "create" || sub === "merge")
|
|
74854
|
+
return later(argv, io);
|
|
74855
|
+
return usage3(io, sub && !sub.startsWith("-") ? `unknown pr subcommand "${sub}"` : "missing pr subcommand", STATUS_USAGE);
|
|
74856
|
+
}
|
|
74857
|
+
var STATUS_USAGE = "workit pr status [--pr <n> | --branch <b>] [--log-lines 60] [--json]", later;
|
|
73025
74858
|
var init_pr = __esm(() => {
|
|
74859
|
+
init_report();
|
|
74860
|
+
init_output();
|
|
74861
|
+
init_forge_common();
|
|
73026
74862
|
init_stub();
|
|
73027
|
-
|
|
74863
|
+
later = notImplemented("pr", "S11");
|
|
73028
74864
|
});
|
|
73029
74865
|
|
|
73030
74866
|
// packages/workit-cli/src/verbs/ci.ts
|
|
@@ -73032,10 +74868,181 @@ var exports_ci = {};
|
|
|
73032
74868
|
__export(exports_ci, {
|
|
73033
74869
|
run: () => run14
|
|
73034
74870
|
});
|
|
73035
|
-
|
|
74871
|
+
async function wait(argv, io) {
|
|
74872
|
+
const flags = parseFlags(argv, {
|
|
74873
|
+
pr: "value",
|
|
74874
|
+
branch: "value",
|
|
74875
|
+
head: "value",
|
|
74876
|
+
timeout: "value",
|
|
74877
|
+
interval: "value"
|
|
74878
|
+
});
|
|
74879
|
+
if (typeof flags === "string")
|
|
74880
|
+
return usage3(io, flags, WAIT_USAGE);
|
|
74881
|
+
if (flags.positionals.length)
|
|
74882
|
+
return usage3(io, `unexpected argument ${flags.positionals[0]}`, WAIT_USAGE);
|
|
74883
|
+
const pr = positiveInt(flags.values.pr, "--pr");
|
|
74884
|
+
if (typeof pr === "string")
|
|
74885
|
+
return usage3(io, pr, WAIT_USAGE);
|
|
74886
|
+
const timeoutMs = parseDuration2(flags.values.timeout ?? "20m");
|
|
74887
|
+
const intervalMs = parseDuration2(flags.values.interval ?? "30s");
|
|
74888
|
+
if (timeoutMs === null || timeoutMs > 6 * 3600000)
|
|
74889
|
+
return usage3(io, "--timeout must be a duration up to 6h (20m, 90s)", WAIT_USAGE);
|
|
74890
|
+
if (intervalMs === null || intervalMs < 1000 || intervalMs > 600000)
|
|
74891
|
+
return usage3(io, "--interval must be between 1s and 10m", WAIT_USAGE);
|
|
74892
|
+
if (pr && flags.values.branch)
|
|
74893
|
+
return usage3(io, "pass --pr or --branch, not both", WAIT_USAGE);
|
|
74894
|
+
const head = flags.values.head ?? null;
|
|
74895
|
+
if (head !== null && !/^[0-9a-f]{7,64}$/u.test(head))
|
|
74896
|
+
return usage3(io, "--head must be a commit sha (7-64 hex chars)", WAIT_USAGE);
|
|
74897
|
+
const connected = connect(io, flags.values.branch);
|
|
74898
|
+
if (!connected.ok)
|
|
74899
|
+
return forgeFail(io, connected);
|
|
74900
|
+
const resolved = connected.data;
|
|
74901
|
+
const number = selectPr(io.cwd, resolved, { pr, branch: flags.values.branch ?? null });
|
|
74902
|
+
if (!number.ok)
|
|
74903
|
+
return forgeFail(io, number);
|
|
74904
|
+
const started = forgeDeps.now();
|
|
74905
|
+
const deadline = started + timeoutMs;
|
|
74906
|
+
resolved.limits.deadline = started + Math.max(timeoutMs, ONE_POLL_MS);
|
|
74907
|
+
let polls = 0;
|
|
74908
|
+
let errors = 0;
|
|
74909
|
+
let status = null;
|
|
74910
|
+
let verdict = { state: "waiting", reason: "not_polled" };
|
|
74911
|
+
for (;; ) {
|
|
74912
|
+
const read = resolved.forge.prStatus(number.data);
|
|
74913
|
+
polls += 1;
|
|
74914
|
+
const elapsedMs = forgeDeps.now() - started;
|
|
74915
|
+
if (read.ok) {
|
|
74916
|
+
errors = 0;
|
|
74917
|
+
status = read.data;
|
|
74918
|
+
const doc = buildStatusDoc(io.cwd, resolved, status, { logLines: 0, behind: false });
|
|
74919
|
+
if (!doc.ok)
|
|
74920
|
+
return forgeFail(io, doc);
|
|
74921
|
+
verdict = waitVerdict(doc.data, { head, elapsedMs });
|
|
74922
|
+
if (verdict.state !== "waiting")
|
|
74923
|
+
break;
|
|
74924
|
+
} else {
|
|
74925
|
+
errors += 1;
|
|
74926
|
+
if (read.code !== "unavailable" && read.code !== "failed")
|
|
74927
|
+
return forgeFail(io, read);
|
|
74928
|
+
if (errors >= MAX_POLL_ERRORS)
|
|
74929
|
+
return forgeFail(io, read, { polls });
|
|
74930
|
+
}
|
|
74931
|
+
const remaining = deadline - forgeDeps.now();
|
|
74932
|
+
if (remaining <= 0)
|
|
74933
|
+
break;
|
|
74934
|
+
await forgeDeps.sleep(Math.min(pollDelay(intervalMs, polls - 1), remaining));
|
|
74935
|
+
}
|
|
74936
|
+
const elapsedMs = forgeDeps.now() - started;
|
|
74937
|
+
if (!status)
|
|
74938
|
+
return emit(io, fail("unavailable", "no successful poll before the timeout", { data: { polls } }));
|
|
74939
|
+
const left = deadline - forgeDeps.now();
|
|
74940
|
+
const built = buildStatusDoc(io.cwd, resolved, status, {
|
|
74941
|
+
logLines: verdict.state === "failed" && left > 0 ? 60 : 0,
|
|
74942
|
+
behind: left >= 1000,
|
|
74943
|
+
behindTimeoutMs: Math.max(1000, Math.min(15000, left)),
|
|
74944
|
+
identity: resolved.identity
|
|
74945
|
+
});
|
|
74946
|
+
if (!built.ok)
|
|
74947
|
+
return forgeFail(io, built);
|
|
74948
|
+
const doc = built.data;
|
|
74949
|
+
const data = {
|
|
74950
|
+
state: verdict.state,
|
|
74951
|
+
reason: verdict.reason,
|
|
74952
|
+
polls,
|
|
74953
|
+
elapsedMs,
|
|
74954
|
+
timeoutMs,
|
|
74955
|
+
status: doc
|
|
74956
|
+
};
|
|
74957
|
+
if (!io.json && verdict.state !== "ready")
|
|
74958
|
+
for (const value of renderStatus(doc))
|
|
74959
|
+
io.stdout(`${value}
|
|
74960
|
+
`);
|
|
74961
|
+
if (verdict.state === "ready")
|
|
74962
|
+
return emit(io, ok(data), () => [
|
|
74963
|
+
`CI ready (${verdict.reason}) after ${polls} poll${polls === 1 ? "" : "s"}, ${seconds(elapsedMs)}`,
|
|
74964
|
+
...renderStatus(doc)
|
|
74965
|
+
]);
|
|
74966
|
+
if (verdict.state === "failed")
|
|
74967
|
+
return emit(io, fail("failed", `CI failed: ${doc.checks.failing.map((check) => check.name).join(", ")}`, {
|
|
74968
|
+
data,
|
|
74969
|
+
unblock: "fix the failure, or if it is a flake: workit ci rerun --failed --reason flake"
|
|
74970
|
+
}));
|
|
74971
|
+
if (verdict.state === "blocked")
|
|
74972
|
+
return emit(io, fail("blocked", `CI wait blocked: ${verdict.reason}`, { data, unblock: verdict.unblock }));
|
|
74973
|
+
return emit(io, fail("pending", `CI still ${verdict.reason.replaceAll("_", " ")} after ${seconds(elapsedMs)}`, {
|
|
74974
|
+
data,
|
|
74975
|
+
unblock: `workit ci wait --pr ${doc.number}${head ? ` --head ${head}` : ""}`
|
|
74976
|
+
}));
|
|
74977
|
+
}
|
|
74978
|
+
async function rerun(argv, io) {
|
|
74979
|
+
const flags = parseFlags(argv, {
|
|
74980
|
+
pr: "value",
|
|
74981
|
+
branch: "value",
|
|
74982
|
+
check: "list",
|
|
74983
|
+
failed: "boolean",
|
|
74984
|
+
reason: "value",
|
|
74985
|
+
force: "boolean"
|
|
74986
|
+
});
|
|
74987
|
+
if (typeof flags === "string")
|
|
74988
|
+
return usage3(io, flags, RERUN_USAGE);
|
|
74989
|
+
if (flags.positionals.length)
|
|
74990
|
+
return usage3(io, `unexpected argument ${flags.positionals[0]}`, RERUN_USAGE);
|
|
74991
|
+
const pr = positiveInt(flags.values.pr, "--pr");
|
|
74992
|
+
if (typeof pr === "string")
|
|
74993
|
+
return usage3(io, pr, RERUN_USAGE);
|
|
74994
|
+
const names = flags.lists.check ?? [];
|
|
74995
|
+
if (pr && flags.values.branch)
|
|
74996
|
+
return usage3(io, "pass --pr or --branch, not both", RERUN_USAGE);
|
|
74997
|
+
if (names.length && flags.booleans.has("failed"))
|
|
74998
|
+
return usage3(io, "pass --check or --failed, not both", RERUN_USAGE);
|
|
74999
|
+
const reason = flags.values.reason;
|
|
75000
|
+
if (reason !== "flake" && reason !== "infra")
|
|
75001
|
+
return usage3(io, "--reason flake|infra is required: rerun only what you judged a flake or an infrastructure failure", RERUN_USAGE);
|
|
75002
|
+
const connected = connect(io, flags.values.branch);
|
|
75003
|
+
if (!connected.ok)
|
|
75004
|
+
return forgeFail(io, connected);
|
|
75005
|
+
const resolved = connected.data;
|
|
75006
|
+
if (resolved.expectedAccount && resolved.identity.login === null)
|
|
75007
|
+
return emit(io, fail("blocked", `identity_unverified: the credential cannot prove it is ${resolved.expectedAccount} (${resolved.identity.note ?? "no /user access"})`, {
|
|
75008
|
+
unblock: `use a credential for ${resolved.expectedAccount} that can read /user (vcs.tokenFile or ${resolved.forge.kind === "github" ? "gh" : "glab"} auth login --hostname ${resolved.forge.apiHost})`
|
|
75009
|
+
}));
|
|
75010
|
+
const number = selectPr(io.cwd, resolved, { pr, branch: flags.values.branch ?? null });
|
|
75011
|
+
if (!number.ok)
|
|
75012
|
+
return forgeFail(io, number);
|
|
75013
|
+
const status = resolved.forge.prStatus(number.data);
|
|
75014
|
+
if (!status.ok)
|
|
75015
|
+
return forgeFail(io, status);
|
|
75016
|
+
const done = executeRerun(io.cwd, resolved, status.data, {
|
|
75017
|
+
failed: names.length === 0,
|
|
75018
|
+
names,
|
|
75019
|
+
reason,
|
|
75020
|
+
force: flags.booleans.has("force")
|
|
75021
|
+
});
|
|
75022
|
+
if (!done.ok)
|
|
75023
|
+
return forgeFail(io, done);
|
|
75024
|
+
const outcome = done.data;
|
|
75025
|
+
return emit(io, ok(outcome), () => [
|
|
75026
|
+
`rerun requested on #${outcome.pr} head ${outcome.head.slice(0, 7)} (${outcome.reason}${outcome.forced ? ", forced" : ""}): ${outcome.rerun.map((check) => check.name).join(", ")}`,
|
|
75027
|
+
...outcome.skipped.length ? [`not rerunnable here: ${outcome.skipped.join(", ")}`] : [],
|
|
75028
|
+
...outcome.recorded ? [] : ["warning: the rerun could not be recorded; the once-per-head guard will not see it"],
|
|
75029
|
+
"next: workit ci wait"
|
|
75030
|
+
]);
|
|
75031
|
+
}
|
|
75032
|
+
async function run14(argv, io) {
|
|
75033
|
+
const [sub, ...rest] = argv;
|
|
75034
|
+
if (sub === "wait")
|
|
75035
|
+
return wait(rest, io);
|
|
75036
|
+
if (sub === "rerun")
|
|
75037
|
+
return rerun(rest, io);
|
|
75038
|
+
return usage3(io, sub && !sub.startsWith("-") ? `unknown ci subcommand "${sub}"` : "missing ci subcommand", `${WAIT_USAGE}
|
|
75039
|
+
${RERUN_USAGE}`);
|
|
75040
|
+
}
|
|
75041
|
+
var WAIT_USAGE = "workit ci wait [--pr <n> | --branch <b>] [--head <sha>] [--timeout 20m] [--interval 30s] [--json]", RERUN_USAGE = "workit ci rerun [--pr <n> | --branch <b>] [--check <name>… | --failed] --reason flake|infra [--force] [--json]", ONE_POLL_MS = 20000, MAX_POLL_ERRORS = 3, seconds = (ms) => `${Math.round(ms / 1000)}s`;
|
|
73036
75042
|
var init_ci = __esm(() => {
|
|
73037
|
-
|
|
73038
|
-
|
|
75043
|
+
init_report();
|
|
75044
|
+
init_output();
|
|
75045
|
+
init_forge_common();
|
|
73039
75046
|
});
|
|
73040
75047
|
|
|
73041
75048
|
// packages/workit-cli/src/verbs/git.ts
|
|
@@ -73071,126 +75078,11 @@ var init_stack = __esm(() => {
|
|
|
73071
75078
|
run17 = notImplemented("stack", "S12");
|
|
73072
75079
|
});
|
|
73073
75080
|
|
|
73074
|
-
// packages/workit-core/src/git/rev.ts
|
|
73075
|
-
import { spawnSync as spawnSync13 } from "node:child_process";
|
|
73076
|
-
import { createHash as createHash12 } from "node:crypto";
|
|
73077
|
-
import fs15 from "node:fs";
|
|
73078
|
-
import os12 from "node:os";
|
|
73079
|
-
import path34 from "node:path";
|
|
73080
|
-
function headSha(cwd) {
|
|
73081
|
-
return line(git(cwd, ["rev-parse", "--verify", "-q", "HEAD^{commit}"]));
|
|
73082
|
-
}
|
|
73083
|
-
function worktreeTree(cwd, options = {}) {
|
|
73084
|
-
const top = line(git(cwd, ["rev-parse", "--show-toplevel"]));
|
|
73085
|
-
const indexPath = line(git(cwd, ["rev-parse", "--path-format=absolute", "--git-path", "index"]));
|
|
73086
|
-
if (!top || !indexPath)
|
|
73087
|
-
return null;
|
|
73088
|
-
const cap = options.maxUntrackedBytes ?? MAX_UNTRACKED_BYTES;
|
|
73089
|
-
const timeoutMs = options.timeoutMs ?? GIT_TIMEOUTS.worktree;
|
|
73090
|
-
const scratch = fs15.mkdtempSync(path34.join(os12.tmpdir(), "workit-tree-"));
|
|
73091
|
-
try {
|
|
73092
|
-
const tempIndex = path34.join(scratch, "index");
|
|
73093
|
-
if (fs15.existsSync(indexPath))
|
|
73094
|
-
seedIndex(indexPath, tempIndex);
|
|
73095
|
-
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
73096
|
-
const untracked = git(top, ["ls-files", "--others", "--exclude-standard", "-z"], {
|
|
73097
|
-
env,
|
|
73098
|
-
timeoutMs
|
|
73099
|
-
});
|
|
73100
|
-
if (!untracked.ok)
|
|
73101
|
-
return null;
|
|
73102
|
-
const skipped = [];
|
|
73103
|
-
for (const file of untracked.stdout.split("\x00").filter(Boolean)) {
|
|
73104
|
-
try {
|
|
73105
|
-
const stat = fs15.statSync(path34.join(top, file));
|
|
73106
|
-
if (stat.isFile() && stat.size > cap)
|
|
73107
|
-
skipped.push({ path: file, size: stat.size, mtimeMs: Math.trunc(stat.mtimeMs) });
|
|
73108
|
-
} catch {}
|
|
73109
|
-
}
|
|
73110
|
-
skipped.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
73111
|
-
const added = skipped.length ? git(top, ["add", "-A", "--pathspec-from-file=-", "--pathspec-file-nul"], {
|
|
73112
|
-
env,
|
|
73113
|
-
timeoutMs,
|
|
73114
|
-
input: [".", ...skipped.map((file) => `:(exclude,literal)${file.path}`)].join("\x00")
|
|
73115
|
-
}) : git(top, ["add", "-A"], { env, timeoutMs });
|
|
73116
|
-
if (!added.ok)
|
|
73117
|
-
return null;
|
|
73118
|
-
const tree = line(git(top, ["write-tree"], { env, timeoutMs }));
|
|
73119
|
-
if (!tree)
|
|
73120
|
-
return null;
|
|
73121
|
-
const base = line(git(top, ["rev-parse", "--verify", "-q", "HEAD^{tree}"])) ?? emptyTree(top);
|
|
73122
|
-
const key = skipped.length ? `sha256:${createHash12("sha256").update(JSON.stringify({ tree, skipped })).digest("hex")}` : tree;
|
|
73123
|
-
return { tree, key, dirty: tree !== base || skipped.length > 0, skipped };
|
|
73124
|
-
} finally {
|
|
73125
|
-
fs15.rmSync(scratch, { recursive: true, force: true });
|
|
73126
|
-
}
|
|
73127
|
-
}
|
|
73128
|
-
function patchId(cwd, base, head) {
|
|
73129
|
-
if (!safeArg(base) || !safeArg(head))
|
|
73130
|
-
return null;
|
|
73131
|
-
const diff = git(cwd, [
|
|
73132
|
-
"diff",
|
|
73133
|
-
"-U3",
|
|
73134
|
-
"--diff-algorithm=myers",
|
|
73135
|
-
"--indent-heuristic",
|
|
73136
|
-
"--no-color",
|
|
73137
|
-
"--no-ext-diff",
|
|
73138
|
-
"--no-textconv",
|
|
73139
|
-
"--no-renames",
|
|
73140
|
-
"--full-index",
|
|
73141
|
-
"--src-prefix=a/",
|
|
73142
|
-
"--dst-prefix=b/",
|
|
73143
|
-
`${base}...${head}`,
|
|
73144
|
-
"--"
|
|
73145
|
-
]);
|
|
73146
|
-
if (!diff.ok || !diff.stdout.trim())
|
|
73147
|
-
return null;
|
|
73148
|
-
const id = git(cwd, ["patch-id", "--stable"], { input: diff.stdout });
|
|
73149
|
-
return line(id)?.split(/\s+/u)[0] ?? null;
|
|
73150
|
-
}
|
|
73151
|
-
function currentBranch2(cwd) {
|
|
73152
|
-
return line(git(cwd, ["symbolic-ref", "--short", "-q", "HEAD"]));
|
|
73153
|
-
}
|
|
73154
|
-
var MAX_BUFFER, GIT_TIMEOUTS, git = (cwd, args, options = {}) => {
|
|
73155
|
-
const result = spawnSync13("git", args, {
|
|
73156
|
-
cwd,
|
|
73157
|
-
encoding: "utf8",
|
|
73158
|
-
env: options.env ?? process.env,
|
|
73159
|
-
input: options.input,
|
|
73160
|
-
maxBuffer: MAX_BUFFER,
|
|
73161
|
-
timeout: options.timeoutMs ?? GIT_TIMEOUTS.local,
|
|
73162
|
-
killSignal: "SIGKILL",
|
|
73163
|
-
windowsHide: true
|
|
73164
|
-
});
|
|
73165
|
-
const error = result.error;
|
|
73166
|
-
return {
|
|
73167
|
-
ok: result.status === 0,
|
|
73168
|
-
stdout: result.stdout ?? "",
|
|
73169
|
-
stderr: result.stderr ?? "",
|
|
73170
|
-
timedOut: error?.code === "ETIMEDOUT"
|
|
73171
|
-
};
|
|
73172
|
-
}, line = (run) => {
|
|
73173
|
-
if (!run.ok)
|
|
73174
|
-
return null;
|
|
73175
|
-
const value = run.stdout.trim();
|
|
73176
|
-
return value ? value : null;
|
|
73177
|
-
}, safeArg = (value) => value.length > 0 && !value.startsWith("-"), emptyTree = (cwd) => line(git(cwd, ["mktree"], { input: "" })), MAX_UNTRACKED_BYTES, seedIndex = (from, to) => {
|
|
73178
|
-
fs15.copyFileSync(from, to);
|
|
73179
|
-
const { atimeNs, mtimeNs } = fs15.statSync(from, { bigint: true });
|
|
73180
|
-
const seconds = (ns) => (Number(ns / 1000n) - 1) / 1e6;
|
|
73181
|
-
fs15.utimesSync(to, seconds(atimeNs), seconds(mtimeNs));
|
|
73182
|
-
};
|
|
73183
|
-
var init_rev = __esm(() => {
|
|
73184
|
-
MAX_BUFFER = 256 * 1024 * 1024;
|
|
73185
|
-
GIT_TIMEOUTS = { local: 20000, worktree: 120000, network: 15000 };
|
|
73186
|
-
MAX_UNTRACKED_BYTES = 5 * 1024 * 1024;
|
|
73187
|
-
});
|
|
73188
|
-
|
|
73189
75081
|
// packages/workit-core/src/ledger.ts
|
|
73190
|
-
import { spawnSync as
|
|
75082
|
+
import { spawnSync as spawnSync15 } from "node:child_process";
|
|
73191
75083
|
import { createHash as createHash13, randomBytes as randomBytes2 } from "node:crypto";
|
|
73192
|
-
import
|
|
73193
|
-
import
|
|
75084
|
+
import fs19 from "node:fs";
|
|
75085
|
+
import path37 from "node:path";
|
|
73194
75086
|
function resolveCommit(cwd, ref) {
|
|
73195
75087
|
if (!safeRef(ref))
|
|
73196
75088
|
return null;
|
|
@@ -73248,16 +75140,16 @@ function storeRoot(cwd) {
|
|
|
73248
75140
|
if (run.ok && common)
|
|
73249
75141
|
return {
|
|
73250
75142
|
ok: true,
|
|
73251
|
-
value: { root:
|
|
75143
|
+
value: { root: path37.join(path37.resolve(cwd, common), "workit"), shared: true }
|
|
73252
75144
|
};
|
|
73253
75145
|
if (!run.ok && NOT_A_REPO.test(run.stderr))
|
|
73254
|
-
return { ok: true, value: { root:
|
|
75146
|
+
return { ok: true, value: { root: path37.join(path37.resolve(cwd), ".workit"), shared: false } };
|
|
73255
75147
|
return err("unavailable", `cannot locate the workit store: git rev-parse failed (${run.stderr.trim().split(`
|
|
73256
75148
|
`)[0] || "no output"})`, "fix the repository (git status should work here) or run from a non-git directory");
|
|
73257
75149
|
}
|
|
73258
75150
|
function ledgerPath(cwd) {
|
|
73259
75151
|
const root = storeRoot(cwd);
|
|
73260
|
-
return root.ok ? { ok: true, value:
|
|
75152
|
+
return root.ok ? { ok: true, value: path37.join(root.value.root, "ledger", "ledger.jsonl") } : root;
|
|
73261
75153
|
}
|
|
73262
75154
|
function actorFromEnv(env) {
|
|
73263
75155
|
const value = (key) => {
|
|
@@ -73270,7 +75162,7 @@ function actorFromEnv(env) {
|
|
|
73270
75162
|
agentId: value("WORKIT_AGENT_ID")
|
|
73271
75163
|
};
|
|
73272
75164
|
}
|
|
73273
|
-
function withLedgerLock(file, fn, waitMs =
|
|
75165
|
+
function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS2) {
|
|
73274
75166
|
const lock = `${file}.lock`;
|
|
73275
75167
|
const deadline = Date.now() + waitMs;
|
|
73276
75168
|
let token = null;
|
|
@@ -73280,7 +75172,7 @@ function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
|
|
|
73280
75172
|
break;
|
|
73281
75173
|
const observed = lockToken(lock);
|
|
73282
75174
|
const age = lockAgeMs(lock);
|
|
73283
|
-
if (age !== null && age >
|
|
75175
|
+
if (age !== null && age > LOCK_STALE_MS2 && ledgerLock.reap(lock, observed))
|
|
73284
75176
|
continue;
|
|
73285
75177
|
if (Date.now() >= deadline)
|
|
73286
75178
|
return null;
|
|
@@ -73294,29 +75186,29 @@ function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
|
|
|
73294
75186
|
}
|
|
73295
75187
|
function appendLine(file, line, options) {
|
|
73296
75188
|
const write = () => {
|
|
73297
|
-
const fd =
|
|
75189
|
+
const fd = fs19.openSync(file, "a+");
|
|
73298
75190
|
try {
|
|
73299
|
-
const size =
|
|
75191
|
+
const size = fs19.fstatSync(fd).size;
|
|
73300
75192
|
let fence = "";
|
|
73301
75193
|
if (size > 0) {
|
|
73302
75194
|
const last = Buffer.alloc(1);
|
|
73303
|
-
|
|
75195
|
+
fs19.readSync(fd, last, 0, 1, size - 1);
|
|
73304
75196
|
if (last[0] !== 10)
|
|
73305
75197
|
fence = `
|
|
73306
75198
|
`;
|
|
73307
75199
|
}
|
|
73308
75200
|
const bytes = Buffer.from(fence + line);
|
|
73309
|
-
const written =
|
|
75201
|
+
const written = fs19.writeSync(fd, bytes);
|
|
73310
75202
|
if (written !== bytes.length)
|
|
73311
75203
|
return err("unavailable", `short write to ${file}: ${written} of ${bytes.length} bytes; the row is torn and will be skipped`);
|
|
73312
75204
|
return { ok: true, value: undefined };
|
|
73313
75205
|
} finally {
|
|
73314
|
-
|
|
75206
|
+
fs19.closeSync(fd);
|
|
73315
75207
|
}
|
|
73316
75208
|
};
|
|
73317
75209
|
try {
|
|
73318
|
-
|
|
73319
|
-
if (!options.forceLock && !onNetworkFs(
|
|
75210
|
+
fs19.mkdirSync(path37.dirname(file), { recursive: true });
|
|
75211
|
+
if (!options.forceLock && !onNetworkFs(path37.dirname(file)))
|
|
73320
75212
|
return write();
|
|
73321
75213
|
const locked = withLedgerLock(file, write);
|
|
73322
75214
|
return locked ?? err("busy", `ledger lock ${file}.lock is held`, "retry; remove the .lock directory if no workit process is running");
|
|
@@ -73355,7 +75247,7 @@ function readLedger(cwd) {
|
|
|
73355
75247
|
return file;
|
|
73356
75248
|
let text = "";
|
|
73357
75249
|
try {
|
|
73358
|
-
text =
|
|
75250
|
+
text = fs19.readFileSync(file.value, "utf8");
|
|
73359
75251
|
} catch (error) {
|
|
73360
75252
|
if (error.code === "ENOENT")
|
|
73361
75253
|
return { ok: true, value: { path: file.value, rows: [], skipped: 0 } };
|
|
@@ -73504,7 +75396,7 @@ function effectiveOf(rows, authors) {
|
|
|
73504
75396
|
const effIndependent = independenceReasons(effective, authors).length === 0;
|
|
73505
75397
|
if (effIndependent && !rowIndependent)
|
|
73506
75398
|
continue;
|
|
73507
|
-
if (effIndependent &&
|
|
75399
|
+
if (effIndependent && FAILING2.has(String(effective.result)) && row.actor.session === effective.actor.session)
|
|
73508
75400
|
continue;
|
|
73509
75401
|
effective = row;
|
|
73510
75402
|
}
|
|
@@ -73556,7 +75448,7 @@ function checkVerdicts(cwd, branch, rows) {
|
|
|
73556
75448
|
}
|
|
73557
75449
|
verdicts.sort((a, b) => a.verdict.seq - b.verdict.seq);
|
|
73558
75450
|
const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
|
|
73559
|
-
const failing = verdicts.find((entry) => entry.current && entry.independent &&
|
|
75451
|
+
const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING2.has(String(entry.verdict.result)));
|
|
73560
75452
|
const newestAccepted = verdicts.findLast((entry) => entry.accepted) ?? null;
|
|
73561
75453
|
const accepted = newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
|
|
73562
75454
|
accepted: false,
|
|
@@ -73713,17 +75605,17 @@ function summarizeRow(row) {
|
|
|
73713
75605
|
};
|
|
73714
75606
|
}
|
|
73715
75607
|
function stackPosition(root, branch) {
|
|
73716
|
-
const dir =
|
|
75608
|
+
const dir = path37.join(root, "stacks");
|
|
73717
75609
|
let names = [];
|
|
73718
75610
|
try {
|
|
73719
|
-
names =
|
|
75611
|
+
names = fs19.readdirSync(dir).filter((name) => name.endsWith(".json"));
|
|
73720
75612
|
} catch {
|
|
73721
75613
|
return null;
|
|
73722
75614
|
}
|
|
73723
75615
|
for (const name of names.toSorted()) {
|
|
73724
75616
|
let parsed;
|
|
73725
75617
|
try {
|
|
73726
|
-
parsed = JSON.parse(
|
|
75618
|
+
parsed = JSON.parse(fs19.readFileSync(path37.join(dir, name), "utf8"));
|
|
73727
75619
|
} catch {
|
|
73728
75620
|
continue;
|
|
73729
75621
|
}
|
|
@@ -73818,13 +75710,13 @@ function buildHandoff(cwd, options = {}) {
|
|
|
73818
75710
|
value: { ...partial, nextCommand, next: options.next?.trim() || nextCommand }
|
|
73819
75711
|
};
|
|
73820
75712
|
}
|
|
73821
|
-
var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, VERDICT_RESULTS, PASSING,
|
|
75713
|
+
var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, VERDICT_RESULTS, PASSING, FAILING2, VERDICT_KINDS, VERDICT_SURFACES, PR_ROW_TYPES, err = (code, error, unblock) => ({
|
|
73822
75714
|
ok: false,
|
|
73823
75715
|
code,
|
|
73824
75716
|
error,
|
|
73825
75717
|
...unblock ? { unblock } : {}
|
|
73826
75718
|
}), gitRun = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
|
|
73827
|
-
const run =
|
|
75719
|
+
const run = spawnSync15("git", args, {
|
|
73828
75720
|
cwd,
|
|
73829
75721
|
encoding: "utf8",
|
|
73830
75722
|
timeout: timeoutMs,
|
|
@@ -73852,20 +75744,20 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, VERDICT_RESULTS, PASSING, FAILING
|
|
|
73852
75744
|
if (process.platform !== "linux")
|
|
73853
75745
|
return false;
|
|
73854
75746
|
try {
|
|
73855
|
-
return isNetworkFsType(
|
|
75747
|
+
return isNetworkFsType(fs19.statfsSync(dir).type);
|
|
73856
75748
|
} catch {
|
|
73857
75749
|
return false;
|
|
73858
75750
|
}
|
|
73859
|
-
},
|
|
75751
|
+
}, LOCK_WAIT_MS2 = 5000, LOCK_STALE_MS2 = 30000, LOCK_OWNER = "owner", lockToken = (lock) => {
|
|
73860
75752
|
try {
|
|
73861
|
-
return
|
|
75753
|
+
return fs19.readFileSync(path37.join(lock, LOCK_OWNER), "utf8");
|
|
73862
75754
|
} catch {
|
|
73863
75755
|
return null;
|
|
73864
75756
|
}
|
|
73865
75757
|
}, lockAgeMs = (lock) => {
|
|
73866
|
-
for (const target of [
|
|
75758
|
+
for (const target of [path37.join(lock, LOCK_OWNER), lock]) {
|
|
73867
75759
|
try {
|
|
73868
|
-
return Date.now() -
|
|
75760
|
+
return Date.now() - fs19.statSync(target).mtimeMs;
|
|
73869
75761
|
} catch {}
|
|
73870
75762
|
}
|
|
73871
75763
|
return null;
|
|
@@ -73951,7 +75843,7 @@ var init_ledger = __esm(() => {
|
|
|
73951
75843
|
"failed"
|
|
73952
75844
|
];
|
|
73953
75845
|
PASSING = new Set(["verified", "tests-verified", "type-check-only"]);
|
|
73954
|
-
|
|
75846
|
+
FAILING2 = new Set(["failed", "blocked"]);
|
|
73955
75847
|
VERDICT_KINDS = ["unit", "live", "perf", "review"];
|
|
73956
75848
|
VERDICT_SURFACES = ["ui", "cli", "api"];
|
|
73957
75849
|
PR_ROW_TYPES = new Set(["pr.created", "pr.status"]);
|
|
@@ -73971,14 +75863,14 @@ var init_ledger = __esm(() => {
|
|
|
73971
75863
|
ledgerLock = {
|
|
73972
75864
|
acquire(lock) {
|
|
73973
75865
|
try {
|
|
73974
|
-
|
|
75866
|
+
fs19.mkdirSync(lock);
|
|
73975
75867
|
} catch (error) {
|
|
73976
75868
|
if (error.code === "EEXIST")
|
|
73977
75869
|
return null;
|
|
73978
75870
|
throw error;
|
|
73979
75871
|
}
|
|
73980
75872
|
const token = `${process.pid}-${randomBytes2(8).toString("hex")}`;
|
|
73981
|
-
|
|
75873
|
+
fs19.writeFileSync(path37.join(lock, LOCK_OWNER), token);
|
|
73982
75874
|
return token;
|
|
73983
75875
|
},
|
|
73984
75876
|
release(lock, token) {
|
|
@@ -73986,35 +75878,35 @@ var init_ledger = __esm(() => {
|
|
|
73986
75878
|
return false;
|
|
73987
75879
|
const tomb = `${lock}.release-${randomBytes2(6).toString("hex")}`;
|
|
73988
75880
|
try {
|
|
73989
|
-
|
|
75881
|
+
fs19.renameSync(lock, tomb);
|
|
73990
75882
|
} catch {
|
|
73991
75883
|
return false;
|
|
73992
75884
|
}
|
|
73993
75885
|
if (lockToken(tomb) !== token) {
|
|
73994
75886
|
try {
|
|
73995
|
-
|
|
75887
|
+
fs19.renameSync(tomb, lock);
|
|
73996
75888
|
} catch {}
|
|
73997
75889
|
return false;
|
|
73998
75890
|
}
|
|
73999
|
-
|
|
75891
|
+
fs19.rmSync(tomb, { recursive: true, force: true });
|
|
74000
75892
|
return true;
|
|
74001
75893
|
},
|
|
74002
75894
|
token: lockToken,
|
|
74003
75895
|
reap(lock, observed) {
|
|
74004
75896
|
const tomb = `${lock}.reap-${randomBytes2(6).toString("hex")}`;
|
|
74005
75897
|
try {
|
|
74006
|
-
|
|
75898
|
+
fs19.renameSync(lock, tomb);
|
|
74007
75899
|
} catch {
|
|
74008
75900
|
return false;
|
|
74009
75901
|
}
|
|
74010
75902
|
if (lockToken(tomb) === observed) {
|
|
74011
|
-
|
|
75903
|
+
fs19.rmSync(tomb, { recursive: true, force: true });
|
|
74012
75904
|
return true;
|
|
74013
75905
|
}
|
|
74014
75906
|
try {
|
|
74015
|
-
|
|
75907
|
+
fs19.renameSync(tomb, lock);
|
|
74016
75908
|
} catch {
|
|
74017
|
-
|
|
75909
|
+
fs19.rmSync(tomb, { recursive: true, force: true });
|
|
74018
75910
|
}
|
|
74019
75911
|
return false;
|
|
74020
75912
|
}
|
|
@@ -74053,34 +75945,34 @@ async function run18(argv, io) {
|
|
|
74053
75945
|
try {
|
|
74054
75946
|
parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
|
|
74055
75947
|
} catch (error) {
|
|
74056
|
-
return
|
|
75948
|
+
return usage4(io, error.message);
|
|
74057
75949
|
}
|
|
74058
75950
|
const { values } = parsed;
|
|
74059
75951
|
let positionals = parsed.positionals;
|
|
74060
75952
|
if (positionals[0] === "add") {
|
|
74061
75953
|
positionals = positionals.slice(1);
|
|
74062
75954
|
if (!["decision", "ruling", "verdict"].includes(positionals[0] ?? ""))
|
|
74063
|
-
return
|
|
75955
|
+
return usage4(io, "ledger add takes decision|ruling|verdict");
|
|
74064
75956
|
}
|
|
74065
75957
|
const [sub, ...rest] = positionals;
|
|
74066
|
-
const pr =
|
|
75958
|
+
const pr = positiveInt2(values.pr, "--pr");
|
|
74067
75959
|
if (pr instanceof Error)
|
|
74068
|
-
return
|
|
74069
|
-
const last =
|
|
75960
|
+
return usage4(io, pr.message);
|
|
75961
|
+
const last = positiveInt2(values.last, "--last");
|
|
74070
75962
|
if (last instanceof Error)
|
|
74071
|
-
return
|
|
75963
|
+
return usage4(io, last.message);
|
|
74072
75964
|
const text = rest.join(" ");
|
|
74073
75965
|
if (sub === undefined)
|
|
74074
|
-
return
|
|
75966
|
+
return usage4(io, "missing subcommand");
|
|
74075
75967
|
if (!["decision", "ruling", "verdict", "list", "show", "check"].includes(sub))
|
|
74076
|
-
return
|
|
75968
|
+
return usage4(io, `unknown ledger subcommand "${sub}"`);
|
|
74077
75969
|
const ledger = readLedger(io.cwd);
|
|
74078
75970
|
if (!ledger.ok)
|
|
74079
75971
|
return failed(io, ledger);
|
|
74080
75972
|
const { rows } = ledger.value;
|
|
74081
75973
|
if (sub === "list" || sub === "show") {
|
|
74082
75974
|
if (rest.length)
|
|
74083
|
-
return
|
|
75975
|
+
return usage4(io, `unexpected argument: ${rest[0]}`);
|
|
74084
75976
|
const listed = filterRows(rows, {
|
|
74085
75977
|
branch: values.branch,
|
|
74086
75978
|
pr,
|
|
@@ -74095,7 +75987,7 @@ async function run18(argv, io) {
|
|
|
74095
75987
|
const isRead = sub === "check" || sub === "verdict" && values.how === undefined && !VERDICT_RESULTS.includes(rest[0] ?? "");
|
|
74096
75988
|
if (isRead) {
|
|
74097
75989
|
if (rest.length > (sub === "verdict" ? 1 : 0))
|
|
74098
|
-
return
|
|
75990
|
+
return usage4(io, `unexpected argument: ${rest.at(-1)}`);
|
|
74099
75991
|
const target = targetBranch(io, rows, rest[0] ?? values.branch, pr);
|
|
74100
75992
|
if (!target.ok)
|
|
74101
75993
|
return failed(io, target);
|
|
@@ -74127,7 +76019,7 @@ async function run18(argv, io) {
|
|
|
74127
76019
|
refs: values.ref
|
|
74128
76020
|
}), (row) => `recorded ruling ${row.id}`);
|
|
74129
76021
|
if (rest.length !== 1)
|
|
74130
|
-
return
|
|
76022
|
+
return usage4(io, `ledger verdict takes one result: ${VERDICT_RESULTS.join("|")}`);
|
|
74131
76023
|
return fromResult(io, recordVerdict(context, {
|
|
74132
76024
|
result: rest[0],
|
|
74133
76025
|
kind: values.kind,
|
|
@@ -74137,12 +76029,12 @@ async function run18(argv, io) {
|
|
|
74137
76029
|
evidenceRefs: values.evidence
|
|
74138
76030
|
}), (row) => `recorded verdict ${row.id}: ${row.result} [${row.kind}] for ${row.branch} @ ${(row.head ?? "").slice(0, 12)}${row.self ? ` (self${row.selfReason === "no_session" ? ": WORKIT_SESSION_ID unset" : ""}; never accepted)` : ""}`);
|
|
74139
76031
|
}
|
|
74140
|
-
var USAGE = "workit ledger decision|ruling|verdict|list|check ... (workit help ledger for the grammar)", OPTIONS,
|
|
76032
|
+
var USAGE = "workit ledger decision|ruling|verdict|list|check ... (workit help ledger for the grammar)", OPTIONS, positiveInt2 = (value, flag) => {
|
|
74141
76033
|
if (value === undefined)
|
|
74142
76034
|
return;
|
|
74143
76035
|
const parsed = Number(value);
|
|
74144
76036
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : new Error(`${flag} must be a positive integer`);
|
|
74145
|
-
}, failed = (io, result) => emit(io, fail(result.code, result.error, result.unblock ? { unblock: result.unblock } : {})), fromResult = (io, result, human) => result.ok ? emit(io, ok(result.value), human) : failed(io, result),
|
|
76037
|
+
}, failed = (io, result) => emit(io, fail(result.code, result.error, result.unblock ? { unblock: result.unblock } : {})), fromResult = (io, result, human) => result.ok ? emit(io, ok(result.value), human) : failed(io, result), usage4 = (io, error) => emit(io, fail("invalid_input", error, { unblock: USAGE })), verdictLines = (check) => {
|
|
74146
76038
|
const lines = [
|
|
74147
76039
|
`${check.branch} @ ${(check.head ?? "no commit").slice(0, 12)}: current: ${check.current.basis}${typeof check.current.verdict?.result === "string" ? ` (${check.current.verdict.result})` : ""}; ${check.accepted.accepted ? "accepted" : `not accepted (${check.accepted.reasons.join(", ")})`}`
|
|
74148
76040
|
];
|
|
@@ -74181,7 +76073,7 @@ __export(exports_handoff, {
|
|
|
74181
76073
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
74182
76074
|
function human(brief) {
|
|
74183
76075
|
const lines = [
|
|
74184
|
-
`branch: ${brief.branch ?? "(detached)"} @ ${
|
|
76076
|
+
`branch: ${brief.branch ?? "(detached)"} @ ${short2(brief.head)}${brief.subject ? ` ${brief.subject}` : ""}`
|
|
74185
76077
|
];
|
|
74186
76078
|
if (brief.base)
|
|
74187
76079
|
lines.push(`base: ${brief.base}${brief.ahead === null ? "" : ` (ahead ${brief.ahead}, behind ${brief.behind})`}`);
|
|
@@ -74252,18 +76144,18 @@ async function run19(argv, io) {
|
|
|
74252
76144
|
await Promise.resolve().then(() => init_task());
|
|
74253
76145
|
return runTaskCommand(["handoff", ...argv]);
|
|
74254
76146
|
}
|
|
74255
|
-
var USAGE2 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]",
|
|
76147
|
+
var USAGE2 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short2 = (sha) => sha ? sha.slice(0, 12) : "no commit";
|
|
74256
76148
|
var init_handoff = __esm(() => {
|
|
74257
76149
|
init_ledger();
|
|
74258
76150
|
init_output();
|
|
74259
76151
|
});
|
|
74260
76152
|
|
|
74261
76153
|
// packages/workit-cli/src/main.ts
|
|
74262
|
-
import
|
|
76154
|
+
import path38 from "node:path";
|
|
74263
76155
|
// packages/workit-cli/package.json
|
|
74264
76156
|
var package_default = {
|
|
74265
76157
|
name: "@brainervirus/workit-cli",
|
|
74266
|
-
version: "2.
|
|
76158
|
+
version: "2.3.0",
|
|
74267
76159
|
private: false,
|
|
74268
76160
|
description: "Workit CLI — setup wizard, doctor, and task control for agentic coding workflows",
|
|
74269
76161
|
keywords: [
|
|
@@ -74402,17 +76294,15 @@ var VERBS = [
|
|
|
74402
76294
|
{
|
|
74403
76295
|
name: "pr",
|
|
74404
76296
|
group: "delivery",
|
|
74405
|
-
usage: "workit pr status
|
|
74406
|
-
summary: "Read PR/MR state
|
|
74407
|
-
planned: "S10/S11",
|
|
76297
|
+
usage: "workit pr status [--pr <n> | --branch <b>] [--log-lines 60] [--json]",
|
|
76298
|
+
summary: "Read PR/MR state: checks with failing log tails, unresolved threads, behind-base, next action",
|
|
74408
76299
|
load: () => Promise.resolve().then(() => (init_pr(), exports_pr))
|
|
74409
76300
|
},
|
|
74410
76301
|
{
|
|
74411
76302
|
name: "ci",
|
|
74412
76303
|
group: "delivery",
|
|
74413
|
-
usage: "workit ci wait
|
|
74414
|
-
summary: "Wait for CI on the
|
|
74415
|
-
planned: "S10",
|
|
76304
|
+
usage: "workit ci wait [--pr <n>] [--head <sha>] [--timeout 20m] | ci rerun [--failed | --check <name>…] --reason flake|infra [--force]",
|
|
76305
|
+
summary: "Wait for CI on the PR head (exit 0/1/3/4), or rerun failed jobs once per head",
|
|
74416
76306
|
load: () => Promise.resolve().then(() => (init_ci(), exports_ci))
|
|
74417
76307
|
},
|
|
74418
76308
|
{
|
|
@@ -74536,7 +76426,7 @@ var defaultIo = (json) => ({
|
|
|
74536
76426
|
});
|
|
74537
76427
|
async function main(argv, overrides = {}, options = {}) {
|
|
74538
76428
|
const parsed = parseGlobals(argv);
|
|
74539
|
-
const cwd =
|
|
76429
|
+
const cwd = path38.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
|
|
74540
76430
|
const io = { ...defaultIo(parsed.json), ...overrides, json: parsed.json, cwd };
|
|
74541
76431
|
if (parsed.error)
|
|
74542
76432
|
return emit(io, fail("invalid_input", parsed.error));
|