@coderook/cli 0.34.11 → 0.36.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/.claude-plugin/plugin.json +1 -1
- package/README.md +71 -0
- package/dist/cbx/src/core/worktree.js +29 -2
- package/dist/cli/src/api.js +16 -0
- package/dist/cli/src/cli.js +97 -0
- package/dist/cli/src/github_bridge.js +403 -0
- package/dist/cli/src/mods_command.js +518 -0
- package/dist/cli/src/zip_writer.js +188 -0
- package/package.json +1 -1
- package/skills/coderook/SKILL.md +45 -1
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.forTests = void 0;
|
|
7
|
+
exports.commandGithub = commandGithub;
|
|
8
|
+
/**
|
|
9
|
+
* `cbx github` — proposals reviewed and tested on GitHub, results on CodeRook.
|
|
10
|
+
*
|
|
11
|
+
* CodeRabbit, Blacksmith and most CI only connect to GitHub-shaped hosts. So
|
|
12
|
+
* rather than each of them learning CodeRook, a GitHub repository is used as
|
|
13
|
+
* a mirror: every open proposal becomes a pull request there, whatever is
|
|
14
|
+
* installed on that repository reviews and builds it, and what they conclude
|
|
15
|
+
* comes back to the proposal as checks — where a required one stops it being
|
|
16
|
+
* applied, exactly like a check reported any other way.
|
|
17
|
+
*
|
|
18
|
+
* It runs on GitHub, in a workflow `setup` writes into the project, so
|
|
19
|
+
* CodeRook never holds a GitHub credential and the mirror costs the service
|
|
20
|
+
* nothing. Each side holds only the other's narrowest key: GitHub keeps a
|
|
21
|
+
* CodeRook token limited to this one project that can read it and report
|
|
22
|
+
* checks, and nothing else; the push token stays in GitHub's secrets.
|
|
23
|
+
*
|
|
24
|
+
* Commits are rebuilt by the git remote helper, which gives the same commit
|
|
25
|
+
* for the same version every time, so the mirror only ever moves forward.
|
|
26
|
+
*/
|
|
27
|
+
const node_child_process_1 = require("node:child_process");
|
|
28
|
+
const node_fs_1 = require("node:fs");
|
|
29
|
+
const promises_1 = require("node:fs/promises");
|
|
30
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
31
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
32
|
+
const node_process_1 = __importDefault(require("node:process"));
|
|
33
|
+
const api_js_1 = require("./api.js");
|
|
34
|
+
const colour_js_1 = require("./colour.js");
|
|
35
|
+
const config_js_1 = require("./config.js");
|
|
36
|
+
const WORKFLOW_PATH = ".github/workflows/coderook-bridge.yml";
|
|
37
|
+
/** The bridge's own job, left out of what is reported back. */
|
|
38
|
+
const BRIDGE_JOB = "sync with CodeRook";
|
|
39
|
+
const MARKER = (id) => `<!-- coderook-proposal:${id} -->`;
|
|
40
|
+
const MARKER_PATTERN = /<!-- coderook-proposal:([0-9a-f-]{36}) -->/;
|
|
41
|
+
const flag = (parsed, name) => {
|
|
42
|
+
const value = parsed.flags.get(name);
|
|
43
|
+
return typeof value === "string" ? value : undefined;
|
|
44
|
+
};
|
|
45
|
+
async function gh(token, route, method = "GET", body) {
|
|
46
|
+
const response = await fetch(`https://api.github.com${route}`, {
|
|
47
|
+
method,
|
|
48
|
+
headers: {
|
|
49
|
+
accept: "application/vnd.github+json",
|
|
50
|
+
authorization: `Bearer ${token}`,
|
|
51
|
+
"x-github-api-version": "2022-11-28",
|
|
52
|
+
"user-agent": "CodeRook-CLI",
|
|
53
|
+
...(body ? { "content-type": "application/json" } : {}),
|
|
54
|
+
},
|
|
55
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
56
|
+
signal: AbortSignal.timeout(30_000),
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
const text = await response.text().catch(() => "");
|
|
60
|
+
let message = text.slice(0, 300);
|
|
61
|
+
try {
|
|
62
|
+
message = JSON.parse(text).message ?? message;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* not JSON */
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`GitHub ${method} ${route.split("?")[0]}: ${response.status} ${message}`);
|
|
68
|
+
}
|
|
69
|
+
return (response.status === 204 ? undefined : await response.json());
|
|
70
|
+
}
|
|
71
|
+
/** Every page of a GitHub list, up to a sensible ceiling. */
|
|
72
|
+
async function ghAll(token, route, key) {
|
|
73
|
+
const all = [];
|
|
74
|
+
for (let page = 1; page <= 10; page += 1) {
|
|
75
|
+
const joiner = route.includes("?") ? "&" : "?";
|
|
76
|
+
const body = await gh(token, `${route}${joiner}per_page=100&page=${page}`);
|
|
77
|
+
const rows = (key ? body[key] : body);
|
|
78
|
+
all.push(...rows);
|
|
79
|
+
if (rows.length < 100)
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
return all;
|
|
83
|
+
}
|
|
84
|
+
function githubToken() {
|
|
85
|
+
for (const name of ["CODEROOK_BRIDGE_PUSH", "GH_TOKEN", "GITHUB_TOKEN"]) {
|
|
86
|
+
const value = node_process_1.default.env[name]?.trim();
|
|
87
|
+
if (value)
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
/* On somebody's own machine, the GitHub CLI's sign-in will do. */
|
|
91
|
+
const asked = (0, node_child_process_1.spawnSync)("gh", ["auth", "token"], { encoding: "utf8" });
|
|
92
|
+
const token = asked.status === 0 ? asked.stdout.trim() : "";
|
|
93
|
+
return token || null;
|
|
94
|
+
}
|
|
95
|
+
/* ------------------------------------------------------------------ git */
|
|
96
|
+
function git(cwd, args, env = node_process_1.default.env, quiet = true) {
|
|
97
|
+
const run = (0, node_child_process_1.spawnSync)("git", args, {
|
|
98
|
+
cwd,
|
|
99
|
+
env,
|
|
100
|
+
encoding: "utf8",
|
|
101
|
+
stdio: quiet ? "pipe" : ["ignore", "inherit", "inherit"],
|
|
102
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
103
|
+
});
|
|
104
|
+
return { ok: run.status === 0, out: run.stdout ?? "", err: run.stderr ?? "" };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The push token as an HTTP header git sends, passed through git's
|
|
108
|
+
* environment-config variables so it never appears in an argument list or a
|
|
109
|
+
* remote URL that `git remote -v` would print.
|
|
110
|
+
*/
|
|
111
|
+
function pushEnvironment(token) {
|
|
112
|
+
const basic = Buffer.from(`x-access-token:${token}`).toString("base64");
|
|
113
|
+
return {
|
|
114
|
+
...node_process_1.default.env,
|
|
115
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
116
|
+
GIT_CONFIG_COUNT: "1",
|
|
117
|
+
GIT_CONFIG_KEY_0: "http.https://github.com/.extraheader",
|
|
118
|
+
GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${basic}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/* ------------------------------------------------------------------ setup */
|
|
122
|
+
function workflow(project, required) {
|
|
123
|
+
return `# Mirrors this project's proposals to pull requests here, so tools that only
|
|
124
|
+
# connect to GitHub (CodeRabbit, Blacksmith, any GitHub Actions CI) can review
|
|
125
|
+
# and test them, and reports what they conclude back to each proposal on
|
|
126
|
+
# CodeRook as checks. Written by \`cbx github setup\`; see
|
|
127
|
+
# https://coderook.com/docs#github-bridge
|
|
128
|
+
#
|
|
129
|
+
# Secrets this needs, in this GitHub repository's settings:
|
|
130
|
+
# CODEROOK_TOKEN a CodeRook token limited to this project (setup made it)
|
|
131
|
+
# CODEROOK_BRIDGE_PUSH a GitHub token that can push and open pull requests here.
|
|
132
|
+
# GitHub's own workflow token cannot: what it pushes starts
|
|
133
|
+
# no other workflow, so CI would never run on a proposal.
|
|
134
|
+
name: CodeRook bridge
|
|
135
|
+
|
|
136
|
+
on:
|
|
137
|
+
schedule:
|
|
138
|
+
- cron: "*/10 * * * *"
|
|
139
|
+
workflow_dispatch:
|
|
140
|
+
pull_request_review:
|
|
141
|
+
types: [submitted]
|
|
142
|
+
|
|
143
|
+
concurrency:
|
|
144
|
+
group: coderook-bridge
|
|
145
|
+
cancel-in-progress: false
|
|
146
|
+
|
|
147
|
+
permissions:
|
|
148
|
+
contents: read
|
|
149
|
+
pull-requests: read
|
|
150
|
+
checks: read
|
|
151
|
+
statuses: read
|
|
152
|
+
|
|
153
|
+
jobs:
|
|
154
|
+
sync:
|
|
155
|
+
name: ${BRIDGE_JOB}
|
|
156
|
+
runs-on: ubuntu-latest
|
|
157
|
+
steps:
|
|
158
|
+
- uses: actions/checkout@v4
|
|
159
|
+
with:
|
|
160
|
+
fetch-depth: 0
|
|
161
|
+
persist-credentials: false
|
|
162
|
+
# What was already imported, so each run fetches only new versions.
|
|
163
|
+
- uses: actions/cache@v4
|
|
164
|
+
with:
|
|
165
|
+
path: .git/coderook
|
|
166
|
+
key: coderook-marks-\${{ github.run_id }}
|
|
167
|
+
restore-keys: coderook-marks-
|
|
168
|
+
- uses: actions/setup-node@v4
|
|
169
|
+
with:
|
|
170
|
+
node-version: 22
|
|
171
|
+
- run: npm install --global @coderook/cli@latest
|
|
172
|
+
- run: cbx github sync
|
|
173
|
+
env:
|
|
174
|
+
CODEROOK_TOKEN: \${{ secrets.CODEROOK_TOKEN }}
|
|
175
|
+
CODEROOK_BRIDGE_PUSH: \${{ secrets.CODEROOK_BRIDGE_PUSH }}
|
|
176
|
+
GITHUB_READ_TOKEN: \${{ github.token }}
|
|
177
|
+
CODEROOK_PROJECT: ${project}
|
|
178
|
+
# Checks that must pass before a proposal can be applied, by name, comma separated:
|
|
179
|
+
# e.g. "github/build, review/coderabbitai[bot]". Everything else is reported as advice.
|
|
180
|
+
CODEROOK_REQUIRED: "${required}"
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
async function setup(parsed) {
|
|
184
|
+
const repo = parsed.positional[1];
|
|
185
|
+
if (!repo || !/^[\w.-]+\/[\w.-]+$/.test(repo)) {
|
|
186
|
+
console.error((0, colour_js_1.red)("Name the GitHub repository to mirror into: cbx github setup <owner>/<repo>"));
|
|
187
|
+
return 1;
|
|
188
|
+
}
|
|
189
|
+
const folder = node_path_1.default.resolve(parsed.positional[2] ?? ".");
|
|
190
|
+
const link = await (0, config_js_1.readLink)(folder);
|
|
191
|
+
if (!link) {
|
|
192
|
+
console.error((0, colour_js_1.red)("This folder is not linked to a project. Run it in the project's folder."));
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
const file = node_path_1.default.join(folder, WORKFLOW_PATH);
|
|
196
|
+
if ((0, node_fs_1.existsSync)(file) && !parsed.flags.has("force")) {
|
|
197
|
+
console.error((0, colour_js_1.red)(`${WORKFLOW_PATH} is already here. Pass --force to write it again.`));
|
|
198
|
+
return 1;
|
|
199
|
+
}
|
|
200
|
+
const token = await (0, api_js_1.createToken)({
|
|
201
|
+
name: `GitHub bridge: ${repo}`.slice(0, 80),
|
|
202
|
+
scopes: ["projects:read", "checks:write"],
|
|
203
|
+
repositoryId: link.repositoryId,
|
|
204
|
+
expiresInDays: 365,
|
|
205
|
+
});
|
|
206
|
+
await (0, promises_1.mkdir)(node_path_1.default.dirname(file), { recursive: true });
|
|
207
|
+
await (0, promises_1.writeFile)(file, workflow(link.slug, flag(parsed, "required") ?? ""), "utf8");
|
|
208
|
+
console.log(`${(0, colour_js_1.green)("Wrote")} ${WORKFLOW_PATH}`);
|
|
209
|
+
/* Straight into GitHub's secrets when the GitHub CLI is here, so the token is never on screen. */
|
|
210
|
+
const stored = (0, node_child_process_1.spawnSync)("gh", ["secret", "set", "CODEROOK_TOKEN", "--repo", repo], {
|
|
211
|
+
input: token.token,
|
|
212
|
+
encoding: "utf8",
|
|
213
|
+
});
|
|
214
|
+
if (stored.status === 0) {
|
|
215
|
+
console.log(`${(0, colour_js_1.green)("Stored")} CODEROOK_TOKEN in ${repo}'s secrets. It reaches ${(0, colour_js_1.bold)(link.slug)} only, and can read it and report checks.`);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
console.log("");
|
|
219
|
+
console.log(`Add this as the secret ${(0, colour_js_1.bold)("CODEROOK_TOKEN")} in ${repo} (Settings → Secrets and variables → Actions).`);
|
|
220
|
+
console.log("It is shown once, reaches this project only, and can read it and report checks:");
|
|
221
|
+
console.log(` ${token.token}`);
|
|
222
|
+
}
|
|
223
|
+
console.log("");
|
|
224
|
+
console.log("Then:");
|
|
225
|
+
console.log(` 1. Add ${(0, colour_js_1.bold)("CODEROOK_BRIDGE_PUSH")}: a GitHub fine-grained token for ${repo} with`);
|
|
226
|
+
console.log(" Contents and Pull requests set to read and write.");
|
|
227
|
+
console.log(` 2. Save the workflow into the project: ${(0, colour_js_1.accent)("cbx submit")}`);
|
|
228
|
+
console.log(` 3. Mirror it once from here, which also puts the workflow on GitHub: ${(0, colour_js_1.accent)(`cbx github sync --repo ${repo}`)}`);
|
|
229
|
+
console.log((0, colour_js_1.dim)(" After that it runs on GitHub every ten minutes, and whenever a review is submitted."));
|
|
230
|
+
console.log((0, colour_js_1.dim)(" Install CodeRabbit on the repository, or use Blacksmith runners in your workflows, as you would anywhere."));
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
function fromCheckRun(run) {
|
|
234
|
+
const state = run.status !== "completed"
|
|
235
|
+
? "pending"
|
|
236
|
+
: ["success", "neutral"].includes(run.conclusion ?? "")
|
|
237
|
+
? "passing"
|
|
238
|
+
: run.conclusion === "skipped"
|
|
239
|
+
? "skipped"
|
|
240
|
+
: "failing";
|
|
241
|
+
return {
|
|
242
|
+
name: `github/${run.name}`,
|
|
243
|
+
state,
|
|
244
|
+
detail: run.output?.title || (run.status === "completed" ? run.conclusion ?? "" : run.status),
|
|
245
|
+
url: run.html_url,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function fromStatus(status) {
|
|
249
|
+
const state = status.state === "success" ? "passing" : status.state === "pending" ? "pending" : "failing";
|
|
250
|
+
return { name: `github/${status.context}`, state, detail: status.description ?? "", url: status.target_url };
|
|
251
|
+
}
|
|
252
|
+
/** The last word from each reviewer: approved passes, changes requested fails, a comment is advice. */
|
|
253
|
+
function fromReviews(reviews) {
|
|
254
|
+
const latest = new Map();
|
|
255
|
+
for (const review of reviews) {
|
|
256
|
+
const login = review.user?.login;
|
|
257
|
+
if (!login || review.state === "PENDING" || review.state === "DISMISSED")
|
|
258
|
+
continue;
|
|
259
|
+
const state = review.state === "APPROVED" ? "passing" : review.state === "CHANGES_REQUESTED" ? "failing" : "passing";
|
|
260
|
+
const said = (review.body ?? "").replace(/\s+/g, " ").trim();
|
|
261
|
+
latest.set(login, {
|
|
262
|
+
name: `review/${login}`,
|
|
263
|
+
state,
|
|
264
|
+
detail: (review.state === "COMMENTED" ? "commented" : review.state.toLowerCase().replace(/_/g, " ")) + (said ? `: ${said}` : ""),
|
|
265
|
+
url: review.html_url,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return [...latest.values()];
|
|
269
|
+
}
|
|
270
|
+
async function sync(parsed) {
|
|
271
|
+
const repo = flag(parsed, "repo") ?? node_process_1.default.env.GITHUB_REPOSITORY;
|
|
272
|
+
if (!repo) {
|
|
273
|
+
console.error((0, colour_js_1.red)("Which GitHub repository? Pass --repo <owner>/<repo>."));
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
const write = githubToken();
|
|
277
|
+
if (!write) {
|
|
278
|
+
console.error((0, colour_js_1.red)("No GitHub token. Set CODEROOK_BRIDGE_PUSH (or GH_TOKEN), or sign in with the GitHub CLI."));
|
|
279
|
+
return 1;
|
|
280
|
+
}
|
|
281
|
+
const github = { repo, write, read: node_process_1.default.env.GITHUB_READ_TOKEN?.trim() || write };
|
|
282
|
+
const required = new Set((flag(parsed, "required") ?? node_process_1.default.env.CODEROOK_REQUIRED ?? "").split(",").map((name) => name.trim()).filter(Boolean));
|
|
283
|
+
/* The project: named, or the one this folder (or the token) belongs to. */
|
|
284
|
+
const named = flag(parsed, "project") ?? node_process_1.default.env.CODEROOK_PROJECT;
|
|
285
|
+
const link = named ? null : await (0, config_js_1.readLink)(node_process_1.default.cwd());
|
|
286
|
+
const listed = await (0, api_js_1.projects)();
|
|
287
|
+
const project = listed.find((one) => one.id === named || one.slug === named) ??
|
|
288
|
+
(link ? listed.find((one) => one.id === link.repositoryId) : undefined) ??
|
|
289
|
+
(listed.length === 1 ? listed[0] : undefined);
|
|
290
|
+
if (!project) {
|
|
291
|
+
console.error((0, colour_js_1.red)(named ? `No project called ${named} on this account.` : "Name the project: --project <slug>."));
|
|
292
|
+
return 1;
|
|
293
|
+
}
|
|
294
|
+
/* A git repository to work in: this one on GitHub's runner, a scratch one on somebody's machine. */
|
|
295
|
+
const here = git(node_process_1.default.cwd(), ["rev-parse", "--is-inside-work-tree"]).ok && Boolean(node_process_1.default.env.GITHUB_ACTIONS);
|
|
296
|
+
const work = here ? node_process_1.default.cwd() : await (0, promises_1.mkdtemp)(node_path_1.default.join(node_os_1.default.tmpdir(), "cbx-github-"));
|
|
297
|
+
try {
|
|
298
|
+
if (!here)
|
|
299
|
+
git(work, ["init", "--quiet"]);
|
|
300
|
+
git(work, ["remote", "remove", "coderook"]);
|
|
301
|
+
git(work, ["remote", "add", "coderook", `coderook://${project.slug}`]);
|
|
302
|
+
console.log(`Fetching ${(0, colour_js_1.bold)(project.slug)} from CodeRook…`);
|
|
303
|
+
const fetched = git(work, ["fetch", "--quiet", "coderook"], node_process_1.default.env, false);
|
|
304
|
+
if (!fetched.ok) {
|
|
305
|
+
console.error((0, colour_js_1.red)("Could not fetch the project from CodeRook."));
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
const open = (await (0, api_js_1.proposals)(project.id)).filter((one) => one.proposal?.from && one.proposal?.into);
|
|
309
|
+
const lines = new Set([project.defaultBranch || "main"]);
|
|
310
|
+
for (const one of open) {
|
|
311
|
+
lines.add(one.proposal.from);
|
|
312
|
+
lines.add(one.proposal.into);
|
|
313
|
+
}
|
|
314
|
+
/* Each line to the branch of the same name. Never forced: the mirror only moves forward. */
|
|
315
|
+
const pushEnv = pushEnvironment(github.write);
|
|
316
|
+
const pushed = new Set();
|
|
317
|
+
for (const line of lines) {
|
|
318
|
+
if (!git(work, ["rev-parse", "--verify", "--quiet", `refs/remotes/coderook/${line}`]).ok)
|
|
319
|
+
continue;
|
|
320
|
+
const result = git(work, ["push", "--quiet", `https://github.com/${repo}.git`, `refs/remotes/coderook/${line}:refs/heads/${line}`], pushEnv);
|
|
321
|
+
if (result.ok) {
|
|
322
|
+
pushed.add(line);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
console.error((0, colour_js_1.accent)(` ${line}: not pushed. ${result.err.trim().split("\n").pop() ?? ""}`));
|
|
326
|
+
console.error((0, colour_js_1.dim)(" The branch on GitHub has commits CodeRook does not. Mirror into a repository nothing else pushes to."));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
console.log(`${(0, colour_js_1.green)("Mirrored")} ${pushed.size} line${pushed.size === 1 ? "" : "s"} to ${repo}`);
|
|
330
|
+
/* Pull requests: one per open proposal, found again by the marker in its description. */
|
|
331
|
+
const existing = await ghAll(github.write, `/repos/${repo}/pulls?state=all`);
|
|
332
|
+
const byProposal = new Map();
|
|
333
|
+
for (const pull of existing) {
|
|
334
|
+
const match = MARKER_PATTERN.exec(pull.body ?? "");
|
|
335
|
+
if (match && !byProposal.has(match[1]))
|
|
336
|
+
byProposal.set(match[1], pull);
|
|
337
|
+
}
|
|
338
|
+
const site = (node_process_1.default.env.CODEROOK_SITE_URL ?? "https://coderook.com").replace(/\/+$/, "");
|
|
339
|
+
let reported = 0;
|
|
340
|
+
for (const one of open) {
|
|
341
|
+
const from = one.proposal.from;
|
|
342
|
+
const into = one.proposal.into;
|
|
343
|
+
if (!pushed.has(from) || !pushed.has(into))
|
|
344
|
+
continue;
|
|
345
|
+
const title = `${one.proposal.title ?? `${from} into ${into}`} (${one.reference})`;
|
|
346
|
+
const body = `Mirrored from CodeRook proposal ${one.reference} so the tools installed here can review and test it. ` +
|
|
347
|
+
`Results are reported back to it as checks; apply it on CodeRook, not here.\n\n` +
|
|
348
|
+
`${site}/merges?merge=${one.id}\n\n${MARKER(one.id)}`;
|
|
349
|
+
let pull = byProposal.get(one.id);
|
|
350
|
+
if (!pull) {
|
|
351
|
+
pull = await gh(github.write, `/repos/${repo}/pulls`, "POST", { title, head: from, base: into, body, maintainer_can_modify: false });
|
|
352
|
+
console.log(`${(0, colour_js_1.green)("Opened")} ${pull.html_url} for ${one.reference}`);
|
|
353
|
+
}
|
|
354
|
+
else if (pull.state !== "open" || pull.title !== title) {
|
|
355
|
+
pull = await gh(github.write, `/repos/${repo}/pulls/${pull.number}`, "PATCH", { title, state: "open", body });
|
|
356
|
+
}
|
|
357
|
+
/* What GitHub's side concluded, back to the proposal; only what changed is reported. */
|
|
358
|
+
const head = (await gh(github.read, `/repos/${repo}/pulls/${pull.number}`)).head.sha;
|
|
359
|
+
const found = [];
|
|
360
|
+
const runs = await ghAll(github.read, `/repos/${repo}/commits/${head}/check-runs`, "check_runs");
|
|
361
|
+
found.push(...runs.filter((run) => run.name !== BRIDGE_JOB).map(fromCheckRun));
|
|
362
|
+
const combined = await gh(github.read, `/repos/${repo}/commits/${head}/status`);
|
|
363
|
+
found.push(...combined.statuses.map(fromStatus));
|
|
364
|
+
found.push(...fromReviews(await ghAll(github.read, `/repos/${repo}/pulls/${pull.number}/reviews`)));
|
|
365
|
+
const current = new Map((await (0, api_js_1.mergeTrack)(one.id)).checks?.map((check) => [check.name, check]) ?? []);
|
|
366
|
+
for (const check of found) {
|
|
367
|
+
const name = check.name.slice(0, 100);
|
|
368
|
+
const detail = check.detail.slice(0, 2000);
|
|
369
|
+
const before = current.get(name);
|
|
370
|
+
const url = check.url && /^https:\/\//.test(check.url) ? check.url : null;
|
|
371
|
+
if (before && before.state === check.state && (before.detail ?? "") === detail)
|
|
372
|
+
continue;
|
|
373
|
+
await (0, api_js_1.reportMergeCheck)(one.id, name, { state: check.state, required: required.has(name), detail, detailsUrl: url });
|
|
374
|
+
reported += 1;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/* A proposal applied or withdrawn on CodeRook closes its pull request here. */
|
|
378
|
+
const stillOpen = new Set(open.map((one) => one.id));
|
|
379
|
+
for (const [id, pull] of byProposal) {
|
|
380
|
+
if (pull.state === "open" && !stillOpen.has(id)) {
|
|
381
|
+
await gh(github.write, `/repos/${repo}/pulls/${pull.number}`, "PATCH", { state: "closed" });
|
|
382
|
+
console.log(`${(0, colour_js_1.dim)("Closed")} ${pull.html_url}: its proposal is no longer open`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
console.log(`${(0, colour_js_1.green)("Reported")} ${reported} check result${reported === 1 ? "" : "s"} to CodeRook`);
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
finally {
|
|
389
|
+
if (!here)
|
|
390
|
+
await (0, promises_1.rm)(work, { recursive: true, force: true }).catch(() => undefined);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
async function commandGithub(parsed) {
|
|
394
|
+
const sub = parsed.positional[0];
|
|
395
|
+
if (sub === "setup")
|
|
396
|
+
return setup(parsed);
|
|
397
|
+
if (sub === "sync")
|
|
398
|
+
return sync(parsed);
|
|
399
|
+
console.error((0, colour_js_1.red)(sub ? `cbx github has no "${sub}". ` : "") + "See: " + (0, colour_js_1.accent)("cbx help github"));
|
|
400
|
+
return 1;
|
|
401
|
+
}
|
|
402
|
+
/** For tests: the workflow text and the mappings. */
|
|
403
|
+
exports.forTests = { workflow, fromCheckRun, fromStatus, fromReviews };
|