gcb-host 0.0.0 → 0.0.1
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/CHANGELOG.md +9 -0
- package/bin/gcb.js +5 -0
- package/lib/audit.d.ts +25 -0
- package/lib/audit.d.ts.map +1 -0
- package/lib/audit.js +73 -0
- package/lib/audit.js.map +1 -0
- package/lib/broker.d.ts +61 -0
- package/lib/broker.d.ts.map +1 -0
- package/lib/broker.js +512 -0
- package/lib/broker.js.map +1 -0
- package/lib/commands/authorize.d.ts +38 -0
- package/lib/commands/authorize.d.ts.map +1 -0
- package/lib/commands/authorize.js +134 -0
- package/lib/commands/authorize.js.map +1 -0
- package/lib/commands/compose.d.ts +42 -0
- package/lib/commands/compose.d.ts.map +1 -0
- package/lib/commands/compose.js +208 -0
- package/lib/commands/compose.js.map +1 -0
- package/lib/commands/deployment.d.ts +49 -0
- package/lib/commands/deployment.d.ts.map +1 -0
- package/lib/commands/deployment.js +72 -0
- package/lib/commands/deployment.js.map +1 -0
- package/lib/commands/diagnose.d.ts +4 -0
- package/lib/commands/diagnose.d.ts.map +1 -0
- package/lib/commands/diagnose.js +213 -0
- package/lib/commands/diagnose.js.map +1 -0
- package/lib/commands/host-opener.d.ts +23 -0
- package/lib/commands/host-opener.d.ts.map +1 -0
- package/lib/commands/host-opener.js +244 -0
- package/lib/commands/host-opener.js.map +1 -0
- package/lib/commands/init.d.ts +153 -0
- package/lib/commands/init.d.ts.map +1 -0
- package/lib/commands/init.js +510 -0
- package/lib/commands/init.js.map +1 -0
- package/lib/commands/stage.d.ts +44 -0
- package/lib/commands/stage.d.ts.map +1 -0
- package/lib/commands/stage.js +165 -0
- package/lib/commands/stage.js.map +1 -0
- package/lib/config.d.ts +110 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +408 -0
- package/lib/config.js.map +1 -0
- package/lib/daemon.d.ts +45 -0
- package/lib/daemon.d.ts.map +1 -0
- package/lib/daemon.js +123 -0
- package/lib/daemon.js.map +1 -0
- package/lib/device-flow.d.ts +80 -0
- package/lib/device-flow.d.ts.map +1 -0
- package/lib/device-flow.js +148 -0
- package/lib/device-flow.js.map +1 -0
- package/lib/gh.d.ts +37 -0
- package/lib/gh.d.ts.map +1 -0
- package/lib/gh.js +181 -0
- package/lib/gh.js.map +1 -0
- package/lib/http.d.ts +20 -0
- package/lib/http.d.ts.map +1 -0
- package/lib/http.js +133 -0
- package/lib/http.js.map +1 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +52 -0
- package/lib/index.js.map +1 -0
- package/lib/policy.d.ts +60 -0
- package/lib/policy.d.ts.map +1 -0
- package/lib/policy.js +118 -0
- package/lib/policy.js.map +1 -0
- package/lib/providers/github-app.d.ts +56 -0
- package/lib/providers/github-app.d.ts.map +1 -0
- package/lib/providers/github-app.js +901 -0
- package/lib/providers/github-app.js.map +1 -0
- package/lib/providers/index.d.ts +29 -0
- package/lib/providers/index.d.ts.map +1 -0
- package/lib/providers/index.js +32 -0
- package/lib/providers/index.js.map +1 -0
- package/lib/providers/static.d.ts +18 -0
- package/lib/providers/static.d.ts.map +1 -0
- package/lib/providers/static.js +41 -0
- package/lib/providers/static.js.map +1 -0
- package/lib/proxy.d.ts +23 -0
- package/lib/proxy.d.ts.map +1 -0
- package/lib/proxy.js +57 -0
- package/lib/proxy.js.map +1 -0
- package/package.json +36 -2
- package/README.md +0 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `diagnose` — explain why the broker could not mint a token.
|
|
3
|
+
*
|
|
4
|
+
* The broker deliberately answers the pushing side with a generic message, because provider
|
|
5
|
+
* errors can embed API responses. This asks GitHub directly and prints what the app can
|
|
6
|
+
* actually see, which is what separates the two causes of
|
|
7
|
+
*
|
|
8
|
+
* 422 "There is at least one repository that does not exist or is not accessible to the
|
|
9
|
+
* parent installation"
|
|
10
|
+
*
|
|
11
|
+
* — the repository was never created, or the installation was granted "only select
|
|
12
|
+
* repositories" and this one is not on that list. The message is identical for both.
|
|
13
|
+
*
|
|
14
|
+
* It never prints a key or a token.
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { assertKeysReadable, loadConfig, resolveConfigPath } from '../config.js';
|
|
18
|
+
import { nextLink, signAppJwt } from '../providers/github-app.js';
|
|
19
|
+
import { fetcherFor, parseProxy } from '../http.js';
|
|
20
|
+
import { fail, parseArgs, say, warn } from 'gcb-core';
|
|
21
|
+
/** Usage text for `diagnose`. */
|
|
22
|
+
export const DIAGNOSE_USAGE = `Usage: gcb diagnose [options]
|
|
23
|
+
|
|
24
|
+
Ask GitHub what this app can actually see: identity, permissions, installations and the
|
|
25
|
+
repositories each installation can reach. Never prints a key or a token.
|
|
26
|
+
|
|
27
|
+
--config <path> The broker configuration to inspect (default: $GIT_BROKER_CONFIG, else ./broker.config.yml)
|
|
28
|
+
--host <host> Which host block to inspect (default: the first github-app one)
|
|
29
|
+
--proxy <url> Use this proxy for this command
|
|
30
|
+
--no-proxy Reach GitHub directly for this command
|
|
31
|
+
-h, --help Show this help
|
|
32
|
+
`;
|
|
33
|
+
/**
|
|
34
|
+
* Run the `diagnose` command.
|
|
35
|
+
*
|
|
36
|
+
* @param argv - arguments after the command name.
|
|
37
|
+
* @returns the process exit code: 0 everything reachable, 1 something is not.
|
|
38
|
+
*/
|
|
39
|
+
import { assertProxyEnabled } from '../proxy.js';
|
|
40
|
+
export async function runDiagnose(argv) {
|
|
41
|
+
const args = parseArgs(argv, { booleans: ['help', 'no-proxy'] });
|
|
42
|
+
if (args.has('no-proxy') && args.value('proxy') !== undefined)
|
|
43
|
+
fail('--proxy and --no-proxy are mutually exclusive');
|
|
44
|
+
if (args.has('proxy') && args.value('proxy') === undefined)
|
|
45
|
+
fail('--proxy needs a value');
|
|
46
|
+
const proxy = args.has('no-proxy') ? null : args.value('proxy');
|
|
47
|
+
if (proxy !== undefined && parseProxy(proxy) === false)
|
|
48
|
+
fail('--proxy must be an http(s) URL');
|
|
49
|
+
if (proxy === undefined)
|
|
50
|
+
assertProxyEnabled();
|
|
51
|
+
if (args.has('help') || args.has('h')) {
|
|
52
|
+
say(DIAGNOSE_USAGE);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
const configPath = resolveConfigPath(args.value('config'));
|
|
56
|
+
const config = loadConfig(configPath);
|
|
57
|
+
// Before reading the key: a configuration written for the sidecar records a path that exists only
|
|
58
|
+
// inside that container, and running this on the host then fails with a bare ENOENT about a path
|
|
59
|
+
// that looks like a mistake in the key rather than in where the command was run.
|
|
60
|
+
assertKeysReadable(config);
|
|
61
|
+
const wanted = args.value('host');
|
|
62
|
+
const entry = Object.entries(config.hosts).find(([host, block]) => block.provider === 'github-app' && (wanted ? host === wanted : true));
|
|
63
|
+
if (!entry)
|
|
64
|
+
fail(`no github-app host${wanted ? ` named ${wanted}` : ''} in ${configPath}`);
|
|
65
|
+
const [host, block] = entry;
|
|
66
|
+
if (!block.privateKeyPath) {
|
|
67
|
+
fail('this host uses privateKeyPem; point the command at a config that references a key file');
|
|
68
|
+
}
|
|
69
|
+
const pem = readFileSync(block.privateKeyPath, 'utf8');
|
|
70
|
+
const issuer = block.clientId ?? block.appId;
|
|
71
|
+
if (issuer === undefined)
|
|
72
|
+
fail('the host block has neither clientId nor appId');
|
|
73
|
+
const api = block.apiBaseUrl ?? 'https://api.github.com';
|
|
74
|
+
const apiVersion = block.apiVersion ?? '2022-11-28';
|
|
75
|
+
const fetcher = fetcherFor(proxy);
|
|
76
|
+
/**
|
|
77
|
+
* Call the API authenticated as the app.
|
|
78
|
+
*
|
|
79
|
+
* @param path - path below the API root.
|
|
80
|
+
* @returns the parsed body and the Link header.
|
|
81
|
+
*/
|
|
82
|
+
async function asApp(path) {
|
|
83
|
+
const jwt = signAppJwt({ privateKeyPem: pem, iss: issuer, nowMs: Date.now() });
|
|
84
|
+
const response = await fetcher(`${api}${path}`, {
|
|
85
|
+
method: 'GET',
|
|
86
|
+
headers: {
|
|
87
|
+
accept: 'application/vnd.github+json',
|
|
88
|
+
authorization: `Bearer ${jwt}`,
|
|
89
|
+
'x-github-api-version': apiVersion,
|
|
90
|
+
'user-agent': 'gcb-diagnose',
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
const text = await response.text();
|
|
94
|
+
let json = null;
|
|
95
|
+
try {
|
|
96
|
+
json = JSON.parse(text);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
json = null;
|
|
100
|
+
}
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
const message = json !== null && typeof json === 'object' && 'message' in json
|
|
103
|
+
? String(json.message)
|
|
104
|
+
: text.slice(0, 200);
|
|
105
|
+
throw new Error(`${path} -> ${response.status}: ${message}`);
|
|
106
|
+
}
|
|
107
|
+
return { json, link: response.headers.get('link') };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Call the API authenticated as one installation.
|
|
111
|
+
*
|
|
112
|
+
* @param installationId - the installation to act as.
|
|
113
|
+
* @param path - path below the API root.
|
|
114
|
+
* @returns the parsed body.
|
|
115
|
+
*/
|
|
116
|
+
async function asInstallation(installationId, path) {
|
|
117
|
+
const jwt = signAppJwt({ privateKeyPem: pem, iss: issuer, nowMs: Date.now() });
|
|
118
|
+
const minted = await fetcher(`${api}/app/installations/${installationId}/access_tokens`, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: {
|
|
121
|
+
accept: 'application/vnd.github+json',
|
|
122
|
+
authorization: `Bearer ${jwt}`,
|
|
123
|
+
'x-github-api-version': apiVersion,
|
|
124
|
+
'user-agent': 'gcb-diagnose',
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
const mintedText = await minted.text();
|
|
128
|
+
if (!minted.ok) {
|
|
129
|
+
throw new Error(`token request for installation ${installationId} -> ${minted.status}: ${mintedText.slice(0, 200)}`);
|
|
130
|
+
}
|
|
131
|
+
const token = JSON.parse(mintedText).token;
|
|
132
|
+
if (!token)
|
|
133
|
+
throw new Error(`installation ${installationId} returned no token`);
|
|
134
|
+
const response = await fetcher(`${api}${path}`, {
|
|
135
|
+
method: 'GET',
|
|
136
|
+
headers: {
|
|
137
|
+
accept: 'application/vnd.github+json',
|
|
138
|
+
authorization: `Bearer ${token}`,
|
|
139
|
+
'x-github-api-version': apiVersion,
|
|
140
|
+
'user-agent': 'gcb-diagnose',
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
const text = await response.text();
|
|
144
|
+
if (!response.ok)
|
|
145
|
+
throw new Error(`${path} -> ${response.status}: ${text.slice(0, 200)}`);
|
|
146
|
+
return JSON.parse(text);
|
|
147
|
+
}
|
|
148
|
+
const app = (await asApp('/app')).json;
|
|
149
|
+
say(`app : ${app.slug ?? '?'} (id ${app.id ?? '?'})`);
|
|
150
|
+
say(`app permissions: ${JSON.stringify(app.permissions ?? {})}`);
|
|
151
|
+
say(`config host : ${host}, allow ${JSON.stringify(block.allow)}`);
|
|
152
|
+
say('');
|
|
153
|
+
const installations = [];
|
|
154
|
+
let page = '/app/installations?per_page=100';
|
|
155
|
+
for (let index = 0; index < 10 && page; index += 1) {
|
|
156
|
+
const result = await asApp(page);
|
|
157
|
+
if (Array.isArray(result.json))
|
|
158
|
+
installations.push(...result.json);
|
|
159
|
+
page = nextLink(result.link);
|
|
160
|
+
}
|
|
161
|
+
if (installations.length === 0) {
|
|
162
|
+
say('installations : none');
|
|
163
|
+
say('');
|
|
164
|
+
say('The app is not installed anywhere. Install it on the account that owns the repositories.');
|
|
165
|
+
return 1;
|
|
166
|
+
}
|
|
167
|
+
/** Repositories each installation can reach. */
|
|
168
|
+
const reachable = new Map();
|
|
169
|
+
for (const installation of installations) {
|
|
170
|
+
say(`installation ${installation.id}`);
|
|
171
|
+
say(` account : ${installation.account?.login ?? '?'} (${installation.account?.type ?? '?'})`);
|
|
172
|
+
say(` repository access : ${installation.repository_selection ?? '?'}`);
|
|
173
|
+
say(` permissions : ${JSON.stringify(installation.permissions ?? {})}`);
|
|
174
|
+
// Enumerate even when the selection is "all". "All" means every repository that *exists*;
|
|
175
|
+
// naming one that does not exist still fails with the same 422, so without this list a
|
|
176
|
+
// repository that was never created looks exactly like one that is merely not selected.
|
|
177
|
+
const listed = (await asInstallation(installation.id, '/installation/repositories?per_page=100'));
|
|
178
|
+
const names = (listed.repositories ?? []).map((repo) => repo.full_name ?? '?');
|
|
179
|
+
reachable.set(installation.id, names);
|
|
180
|
+
const shown = names.slice(0, 20).join(', ');
|
|
181
|
+
say(` repositories (${names.length}${names.length >= 100 ? '+' : ''}): ${shown || '(none)'}${names.length > 20 ? ', …' : ''}`);
|
|
182
|
+
}
|
|
183
|
+
say('');
|
|
184
|
+
say('verdict:');
|
|
185
|
+
let problems = 0;
|
|
186
|
+
for (const allowEntry of block.allow) {
|
|
187
|
+
const [owner, repoPattern] = allowEntry.split('/');
|
|
188
|
+
const matching = installations.filter((installation) => installation.account?.login?.toLowerCase() === owner?.toLowerCase());
|
|
189
|
+
if (matching.length === 0) {
|
|
190
|
+
say(` ${allowEntry}: no installation on account "${owner}"`);
|
|
191
|
+
problems += 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const visible = matching.some((installation) => {
|
|
195
|
+
const known = reachable.get(installation.id) ?? [];
|
|
196
|
+
if (repoPattern === '*') {
|
|
197
|
+
return known.some((name) => name.toLowerCase().startsWith(`${owner?.toLowerCase()}/`));
|
|
198
|
+
}
|
|
199
|
+
return known.some((name) => name.toLowerCase() === `${owner?.toLowerCase()}/${repoPattern?.toLowerCase()}`);
|
|
200
|
+
});
|
|
201
|
+
say(` ${allowEntry}: ${visible ? 'reachable' : 'NOT in the repositories this installation can see'}`);
|
|
202
|
+
if (!visible)
|
|
203
|
+
problems += 1;
|
|
204
|
+
}
|
|
205
|
+
if (problems > 0) {
|
|
206
|
+
warn('');
|
|
207
|
+
warn('A repository that is not listed above either does not exist yet, or the installation was');
|
|
208
|
+
warn('granted "Only select repositories" and this one is not on that list — GitHub returns the');
|
|
209
|
+
warn('same 422 for both. To grant access: organisation settings -> GitHub Apps -> Configure.');
|
|
210
|
+
}
|
|
211
|
+
return problems > 0 ? 1 : 0;
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=diagnose.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnose.js","sourceRoot":"","sources":["../../src/commands/diagnose.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjF,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEpD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAgBtD,iCAAiC;AACjC,MAAM,CAAC,MAAM,cAAc,GAAG;;;;;;;;;;CAU7B,CAAC;AAEF;;;;;GAKG;AACH,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAuB;IACvD,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC,CAAC;IACjE,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,SAAS;QAAE,IAAI,CAAC,+CAA+C,CAAC,CAAC;IACrH,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,SAAS;QAAE,IAAI,CAAC,uBAAuB,CAAC,CAAC;IAC1F,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAChE,IAAI,KAAK,KAAK,SAAS,IAAI,UAAU,CAAC,KAAK,CAAC,KAAK,KAAK;QAAE,IAAI,CAAC,gCAAgC,CAAC,CAAC;IAC/F,IAAI,KAAK,KAAK,SAAS;QAAE,kBAAkB,EAAE,CAAC;IAC9C,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QACtC,GAAG,CAAC,cAAc,CAAC,CAAC;QACpB,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,UAAU,GAAG,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;IAE3D,MAAM,MAAM,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IACtC,kGAAkG;IAClG,iGAAiG;IACjG,iFAAiF;IACjF,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC3B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAClC,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAC7C,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,YAAY,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CACxF,CAAC;IACF,IAAI,CAAC,KAAK;QAAE,IAAI,CAAC,qBAAqB,MAAM,CAAC,CAAC,CAAC,UAAU,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,UAAU,EAAE,CAAC,CAAC;IAC3F,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,KAAsC,CAAC;IAE7D,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC;QAC1B,IAAI,CAAC,wFAAwF,CAAC,CAAC;IACjG,CAAC;IACD,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;IACvD,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,KAAK,CAAC;IAC7C,IAAI,MAAM,KAAK,SAAS;QAAE,IAAI,CAAC,+CAA+C,CAAC,CAAC;IAChF,MAAM,GAAG,GAAG,KAAK,CAAC,UAAU,IAAI,wBAAwB,CAAC;IACzD,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,IAAI,YAAY,CAAC;IACpD,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;IAElC;;;;;OAKG;IACH,KAAK,UAAU,KAAK,CAAC,IAAY;QAC/B,MAAM,GAAG,GAAG,UAAU,CAAC,EAAE,aAAa,EAAE,GAAG,EAAE,GAAG,EAAE,MAAyB,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QAClG,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,GAAG,GAAG,IAAI,EAAE,EAAE;YAC9C,MAAM,EAAE,KAAK;YACb,OAAO,EAAE;gBACP,MAAM,EAAE,6BAA6B;gBACrC,aAAa,EAAE,UAAU,GAAG,EAAE;gBAC9B,sBAAsB,EAAE,UAAU;gBAClC,YAAY,EAAE,cAAc;aAC7B;SACF,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,IAAI,IAAI,GAAY,IAAI,CAAC;QACzB,IAAI,CAAC;YACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,GAAG,IAAI,CAAC;QACd,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,OAAO,GACX,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,SAAS,IAAI,IAAI;gBAC5D,CAAC,CAAC,MAAM,CAAE,IAA6B,CAAC,OAAO,CAAC;gBAChD,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,OAAO,QAAQ,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC,CAAC;QAC/D,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;IACtD,CAAC;IAED;;;;;;OAMG;IACH,KAAK,UAAU,cAAc,CAAC,cAAsB,EAAE,IAAY;QAChE,MAAM,GAAG,GAAG,UAAU,CAAC,EAAE,aAAa,EAAE,GAAG,EAAE,GAAG,EAAE,MAAyB,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QAClG,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,GAAG,sBAAsB,cAAc,gBAAgB,EAAE;YACvF,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,MAAM,EAAE,6BAA6B;gBACrC,aAAa,EAAE,UAAU,GAAG,EAAE;gBAC9B,sBAAsB,EAAE,UAAU;gBAClC,YAAY,EAAE,cAAc;aAC7B;SACF,CAAC,CAAC;QACH,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACvC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,kCAAkC,cAAc,OAAO,MAAM,CAAC,MAAM,KAAK,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QACvH,CAAC;QACD,MAAM,KAAK,GAAI,IAAI,CAAC,KAAK,CAAC,UAAU,CAAwB,CAAC,KAAK,CAAC;QACnE,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,cAAc,oBAAoB,CAAC,CAAC;QAEhF,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,GAAG,GAAG,IAAI,EAAE,EAAE;YAC9C,MAAM,EAAE,KAAK;YACb,OAAO,EAAE;gBACP,MAAM,EAAE,6BAA6B;gBACrC,aAAa,EAAE,UAAU,KAAK,EAAE;gBAChC,sBAAsB,EAAE,UAAU;gBAClC,YAAY,EAAE,cAAc;aAC7B;SACF,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,OAAO,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QAC1F,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC;IACrC,CAAC;IAED,MAAM,GAAG,GAAG,CAAC,MAAM,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAA4E,CAAC;IAC/G,GAAG,CAAC,oBAAoB,GAAG,CAAC,IAAI,IAAI,GAAG,QAAQ,GAAG,CAAC,EAAE,IAAI,GAAG,GAAG,CAAC,CAAC;IACjE,GAAG,CAAC,oBAAoB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;IACjE,GAAG,CAAC,oBAAoB,IAAI,WAAW,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACtE,GAAG,CAAC,EAAE,CAAC,CAAC;IAER,MAAM,aAAa,GAAmB,EAAE,CAAC;IACzC,IAAI,IAAI,GAAkB,iCAAiC,CAAC;IAC5D,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,EAAE,IAAI,IAAI,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACnD,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC;YAAE,aAAa,CAAC,IAAI,CAAC,GAAI,MAAM,CAAC,IAAuB,CAAC,CAAC;QACvF,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IAED,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,GAAG,CAAC,uBAAuB,CAAC,CAAC;QAC7B,GAAG,CAAC,EAAE,CAAC,CAAC;QACR,GAAG,CAAC,0FAA0F,CAAC,CAAC;QAChG,OAAO,CAAC,CAAC;IACX,CAAC;IAED,gDAAgD;IAChD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAoB,CAAC;IAE9C,KAAK,MAAM,YAAY,IAAI,aAAa,EAAE,CAAC;QACzC,GAAG,CAAC,gBAAgB,YAAY,CAAC,EAAE,EAAE,CAAC,CAAC;QACvC,GAAG,CAAC,0BAA0B,YAAY,CAAC,OAAO,EAAE,KAAK,IAAI,GAAG,KAAK,YAAY,CAAC,OAAO,EAAE,IAAI,IAAI,GAAG,GAAG,CAAC,CAAC;QAC3G,GAAG,CAAC,0BAA0B,YAAY,CAAC,oBAAoB,IAAI,GAAG,EAAE,CAAC,CAAC;QAC1E,GAAG,CAAC,0BAA0B,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,WAAW,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;QAEhF,0FAA0F;QAC1F,uFAAuF;QACvF,wFAAwF;QACxF,MAAM,MAAM,GAAG,CAAC,MAAM,cAAc,CAAC,YAAY,CAAC,EAAE,EAAE,yCAAyC,CAAC,CAE/F,CAAC;QACF,MAAM,KAAK,GAAG,CAAC,MAAM,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,IAAI,GAAG,CAAC,CAAC;QAC/E,SAAS,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QACtC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5C,GAAG,CACD,mBAAmB,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,QAAQ,GAAG,KAAK,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAC3H,CAAC;IACJ,CAAC;IAED,GAAG,CAAC,EAAE,CAAC,CAAC;IACR,GAAG,CAAC,UAAU,CAAC,CAAC;IAChB,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,KAAK,MAAM,UAAU,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;QACrC,MAAM,CAAC,KAAK,EAAE,WAAW,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACnD,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CACnC,CAAC,YAAY,EAAE,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,KAAK,EAAE,WAAW,EAAE,CACtF,CAAC;QACF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,GAAG,CAAC,KAAK,UAAU,iCAAiC,KAAK,GAAG,CAAC,CAAC;YAC9D,QAAQ,IAAI,CAAC,CAAC;YACd,SAAS;QACX,CAAC;QACD,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,YAAY,EAAE,EAAE;YAC7C,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;YACnD,IAAI,WAAW,KAAK,GAAG,EAAE,CAAC;gBACxB,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,GAAG,KAAK,EAAE,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC;YACzF,CAAC;YACD,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,EAAE,WAAW,EAAE,IAAI,WAAW,EAAE,WAAW,EAAE,EAAE,CAAC,CAAC;QAC9G,CAAC,CAAC,CAAC;QACH,GAAG,CAAC,KAAK,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,mDAAmD,EAAE,CAAC,CAAC;QACvG,IAAI,CAAC,OAAO;YAAE,QAAQ,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;QACjB,IAAI,CAAC,EAAE,CAAC,CAAC;QACT,IAAI,CAAC,0FAA0F,CAAC,CAAC;QACjG,IAAI,CAAC,0FAA0F,CAAC,CAAC;QACjG,IAAI,CAAC,wFAAwF,CAAC,CAAC;IACjG,CAAC;IACD,OAAO,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9B,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Usage text for `host-opener`. */
|
|
2
|
+
export declare const HOST_OPENER_USAGE = "Usage: gcb host-opener [options]\n\nRun on the host, once, beside the trees the container also sees. It finds the checkouts under the\nroots it is given \u2014 ones created later included \u2014 and fulfils their requests to open a pull request\nwith the credentials of whoever runs it, so the pull request is authored by a person rather than by\nthe app's bot. It does nothing else: it cannot push, merge or close anything.\n\n --root <dir> A tree to serve, repeatable (default: $HOME)\n --depth <n> How far below a root to look for checkouts (default: 4)\n --repo <path> Serve this one checkout instead of discovering any\n --command <path> The program that creates a pull request\n (default: $GIT_BROKER_PR_COMMAND, else gh)\n --arg <token> An extra argument for that program, before the subcommand (repeatable)\n --interval <seconds> How often to look for requests (default: 5)\n --once Fulfil what is waiting, then exit\n -h, --help Show this help\n\nThe container asks for this by passing --via-host to `pr`. Requests are files under\n<git dir>/gcb/pr-requests, which is shared with the container and untracked by git\n\u2014 so the paths on the two sides never have to match: each writes and reads its own view of the same\ndirectory, and only the roots this process scans are host-side paths.\n";
|
|
3
|
+
/**
|
|
4
|
+
* Read a pull request's number and URL out of a program's output.
|
|
5
|
+
*
|
|
6
|
+
* `gh pr create` prints the URL, and so does anything that behaves like it. Nothing else is trusted:
|
|
7
|
+
* without a URL in the output the opener reports a failure rather than guessing.
|
|
8
|
+
*
|
|
9
|
+
* @param output - the program's standard output.
|
|
10
|
+
* @returns the number and URL, or `undefined`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function pullRequestUrl(output: string): {
|
|
13
|
+
number: number;
|
|
14
|
+
url: string;
|
|
15
|
+
} | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Run the `host-opener` command.
|
|
18
|
+
*
|
|
19
|
+
* @param argv - arguments after the command name.
|
|
20
|
+
* @returns the process exit code.
|
|
21
|
+
*/
|
|
22
|
+
export declare function runHostOpener(argv: readonly string[]): Promise<number>;
|
|
23
|
+
//# sourceMappingURL=host-opener.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-opener.d.ts","sourceRoot":"","sources":["../../src/commands/host-opener.ts"],"names":[],"mappings":"AAwCA,oCAAoC;AACpC,eAAO,MAAM,iBAAiB,63CAqB7B,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAK1F;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuE5E"}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `host-opener` — fulfil the container's requests to open pull requests, as the person running it.
|
|
3
|
+
*
|
|
4
|
+
* This runs on the host, beside the checkout the container also sees, and it does exactly one thing:
|
|
5
|
+
* it turns a request file into a pull request, using the credentials of whoever started it. That is
|
|
6
|
+
* the whole point — a pull request authored by a person is one that automated reviewers will actually
|
|
7
|
+
* review, and the app cannot be that person.
|
|
8
|
+
*
|
|
9
|
+
* It has no other power. It cannot push, merge, close or read anything, and it never runs a shell: the
|
|
10
|
+
* program it calls is configuration, and it is called with an argument list built here.
|
|
11
|
+
*/
|
|
12
|
+
import { execFile } from 'node:child_process';
|
|
13
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { homedir, tmpdir } from 'node:os';
|
|
15
|
+
import { join, resolve } from 'node:path';
|
|
16
|
+
import { promisify } from 'node:util';
|
|
17
|
+
import { channelDir, clearOpener, discoverCheckouts, findGitDir, listRequests, readOpener, claimRequest, removeRequest, requestProblems, writeOpener, writeResult, } from 'gcb-core';
|
|
18
|
+
import { fail, listFlag, parseArgs, say, warn } from 'gcb-core';
|
|
19
|
+
import { applyProxyChoice, parseProxy } from '../http.js';
|
|
20
|
+
const run = promisify(execFile);
|
|
21
|
+
/** How long one `pr create` may take. */
|
|
22
|
+
const CREATE_TIMEOUT_MS = 120_000;
|
|
23
|
+
/** Usage text for `host-opener`. */
|
|
24
|
+
export const HOST_OPENER_USAGE = `Usage: gcb host-opener [options]
|
|
25
|
+
|
|
26
|
+
Run on the host, once, beside the trees the container also sees. It finds the checkouts under the
|
|
27
|
+
roots it is given — ones created later included — and fulfils their requests to open a pull request
|
|
28
|
+
with the credentials of whoever runs it, so the pull request is authored by a person rather than by
|
|
29
|
+
the app's bot. It does nothing else: it cannot push, merge or close anything.
|
|
30
|
+
|
|
31
|
+
--root <dir> A tree to serve, repeatable (default: $HOME)
|
|
32
|
+
--depth <n> How far below a root to look for checkouts (default: 4)
|
|
33
|
+
--repo <path> Serve this one checkout instead of discovering any
|
|
34
|
+
--command <path> The program that creates a pull request
|
|
35
|
+
(default: $GIT_BROKER_PR_COMMAND, else gh)
|
|
36
|
+
--arg <token> An extra argument for that program, before the subcommand (repeatable)
|
|
37
|
+
--interval <seconds> How often to look for requests (default: 5)
|
|
38
|
+
--once Fulfil what is waiting, then exit
|
|
39
|
+
-h, --help Show this help
|
|
40
|
+
|
|
41
|
+
The container asks for this by passing --via-host to \`pr\`. Requests are files under
|
|
42
|
+
<git dir>/gcb/pr-requests, which is shared with the container and untracked by git
|
|
43
|
+
— so the paths on the two sides never have to match: each writes and reads its own view of the same
|
|
44
|
+
directory, and only the roots this process scans are host-side paths.
|
|
45
|
+
`;
|
|
46
|
+
/**
|
|
47
|
+
* Read a pull request's number and URL out of a program's output.
|
|
48
|
+
*
|
|
49
|
+
* `gh pr create` prints the URL, and so does anything that behaves like it. Nothing else is trusted:
|
|
50
|
+
* without a URL in the output the opener reports a failure rather than guessing.
|
|
51
|
+
*
|
|
52
|
+
* @param output - the program's standard output.
|
|
53
|
+
* @returns the number and URL, or `undefined`.
|
|
54
|
+
*/
|
|
55
|
+
export function pullRequestUrl(output) {
|
|
56
|
+
const match = /https:\/\/[^\s]+\/pull\/(\d+)/.exec(output);
|
|
57
|
+
const digits = match?.[1];
|
|
58
|
+
if (!match || digits === undefined)
|
|
59
|
+
return undefined;
|
|
60
|
+
return { number: Number(digits), url: match[0] };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Run the `host-opener` command.
|
|
64
|
+
*
|
|
65
|
+
* @param argv - arguments after the command name.
|
|
66
|
+
* @returns the process exit code.
|
|
67
|
+
*/
|
|
68
|
+
export async function runHostOpener(argv) {
|
|
69
|
+
const args = parseArgs(argv, { booleans: ['help', 'once'] });
|
|
70
|
+
if (args.has('help') || args.has('h')) {
|
|
71
|
+
say(HOST_OPENER_USAGE);
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
const explicit = args.value('repo');
|
|
75
|
+
const single = explicit === undefined ? undefined : resolve(explicit);
|
|
76
|
+
const roots = listFlag(args, 'root');
|
|
77
|
+
const scanned = roots.length > 0 ? roots.map((root) => resolve(root)) : [homedir()];
|
|
78
|
+
const depth = Number(args.value('depth') ?? '4');
|
|
79
|
+
const command = args.value('command') ?? process.env['GIT_BROKER_PR_COMMAND'] ?? 'gh';
|
|
80
|
+
const extra = listFlag(args, 'arg');
|
|
81
|
+
const interval = Number(args.value('interval') ?? '5');
|
|
82
|
+
if (!Number.isFinite(interval) || interval < 1) {
|
|
83
|
+
fail(`--interval must be at least one second, got ${JSON.stringify(args.value('interval'))}`);
|
|
84
|
+
}
|
|
85
|
+
if (!Number.isInteger(depth) || depth < 0) {
|
|
86
|
+
fail(`--depth must be a whole number of levels, got ${JSON.stringify(args.value('depth'))}`);
|
|
87
|
+
}
|
|
88
|
+
/** The checkouts to serve: one named checkout, or everything under the roots. */
|
|
89
|
+
const targets = () => {
|
|
90
|
+
if (single === undefined)
|
|
91
|
+
return scanned.flatMap((root) => discoverCheckouts(root, depth));
|
|
92
|
+
const gitDir = findGitDir(single);
|
|
93
|
+
if (gitDir === undefined)
|
|
94
|
+
fail(`no git repository at or above ${single}`);
|
|
95
|
+
return [{ repo: single, dir: channelDir(gitDir) }];
|
|
96
|
+
};
|
|
97
|
+
const advertised = new Set();
|
|
98
|
+
/** Advertise a channel, without rewriting the marker on every sweep. */
|
|
99
|
+
const advertise = (dir) => {
|
|
100
|
+
advertised.add(dir);
|
|
101
|
+
if (readOpener(dir)?.pid !== process.pid) {
|
|
102
|
+
writeOpener(dir, { pid: process.pid, command, startedAt: new Date().toISOString() });
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
// However this process ends — a signal, --once, an exception — its markers go with it. A stale
|
|
106
|
+
// marker would tell the container an opener is there and make it wait for an answer that is not
|
|
107
|
+
// coming.
|
|
108
|
+
process.on('exit', () => {
|
|
109
|
+
for (const dir of advertised) {
|
|
110
|
+
try {
|
|
111
|
+
if (readOpener(dir)?.pid === process.pid)
|
|
112
|
+
clearOpener(dir);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Nothing useful is left to do while exiting.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
const stop = () => process.exit(0);
|
|
120
|
+
process.on('SIGINT', stop);
|
|
121
|
+
process.on('SIGTERM', stop);
|
|
122
|
+
say(single === undefined ? `serving the checkouts under ${scanned.join(', ')}` : `serving ${single}`);
|
|
123
|
+
say(`creating pull requests with ${command}${extra.length > 0 ? ` ${extra.join(' ')}` : ''}, as ${process.env['USER'] ?? 'this user'}`);
|
|
124
|
+
for (;;) {
|
|
125
|
+
for (const { repo, dir } of targets()) {
|
|
126
|
+
// One checkout the container has made hostile — its channel directory replaced by a file, say —
|
|
127
|
+
// must not stop the opener serving every other root, under Restart=always least of all.
|
|
128
|
+
try {
|
|
129
|
+
advertise(dir);
|
|
130
|
+
await sweep(dir, repo, command, extra);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
warn(`could not serve ${repo}: ${error.message}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (args.has('once'))
|
|
137
|
+
return 0;
|
|
138
|
+
await new Promise((done) => setTimeout(done, Math.max(interval, 0) * 1000));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Answer everything waiting in the channel.
|
|
143
|
+
*
|
|
144
|
+
* @param dir - the channel directory.
|
|
145
|
+
* @param repo - the checkout to create pull requests in.
|
|
146
|
+
* @param command - the program to call.
|
|
147
|
+
* @param extra - arguments to pass before the subcommand.
|
|
148
|
+
* @returns how many requests were answered.
|
|
149
|
+
*/
|
|
150
|
+
async function sweep(dir, repo, command, extra) {
|
|
151
|
+
let answered = 0;
|
|
152
|
+
for (const id of listRequests(dir)) {
|
|
153
|
+
// Claimed, not read: two openers — a service and a hand-run --once — would otherwise both act on the
|
|
154
|
+
// same request and each create the pull request, under the person's name.
|
|
155
|
+
const request = claimRequest(dir, id);
|
|
156
|
+
if (!request) {
|
|
157
|
+
// Either unreadable or already claimed, and the two are not worth telling apart here: removing
|
|
158
|
+
// anything now would delete another opener's claim, which is the claim's whole purpose.
|
|
159
|
+
warn(`skipped ${id}: unreadable, or another opener has it`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const problems = requestProblems(request);
|
|
163
|
+
if (problems.length > 0) {
|
|
164
|
+
writeResult(dir, id, { error: problems.join('; ') });
|
|
165
|
+
removeRequest(dir, id);
|
|
166
|
+
warn(`refused ${id}: ${problems.join('; ')}`);
|
|
167
|
+
answered += 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const proxy = parseProxy(request.proxy);
|
|
171
|
+
if (proxy === false) {
|
|
172
|
+
writeResult(dir, id, { error: 'invalid proxy in the request' });
|
|
173
|
+
removeRequest(dir, id);
|
|
174
|
+
warn(`refused ${id}: invalid proxy`);
|
|
175
|
+
answered += 1;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const result = await openOne(repo, command, extra, { ...request, proxy });
|
|
179
|
+
writeResult(dir, id, result);
|
|
180
|
+
removeRequest(dir, id);
|
|
181
|
+
if (result.error !== undefined) {
|
|
182
|
+
warn(`failed to open ${request.head}: ${result.error}`);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
say(`opened #${result.number}: ${result.url}`);
|
|
186
|
+
}
|
|
187
|
+
answered += 1;
|
|
188
|
+
}
|
|
189
|
+
return answered;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Create one pull request.
|
|
193
|
+
*
|
|
194
|
+
* @param repo - the checkout to create it in.
|
|
195
|
+
* @param command - the program to call.
|
|
196
|
+
* @param extra - arguments to pass before the subcommand.
|
|
197
|
+
* @param request - what to create.
|
|
198
|
+
* @returns the number and URL, or the reason it did not work.
|
|
199
|
+
*/
|
|
200
|
+
function openerEnv(proxy) {
|
|
201
|
+
const env = { ...process.env };
|
|
202
|
+
applyProxyChoice(env, proxy);
|
|
203
|
+
return env;
|
|
204
|
+
}
|
|
205
|
+
async function openOne(repo, command, extra, request) {
|
|
206
|
+
// The body goes to a private temp file: the program is given a path, never a string to interpret.
|
|
207
|
+
const scratch = mkdtempSync(join(tmpdir(), 'gcb-'));
|
|
208
|
+
const bodyFile = join(scratch, 'body.md');
|
|
209
|
+
writeFileSync(bodyFile, request.body, 'utf8');
|
|
210
|
+
try {
|
|
211
|
+
const { stdout, stderr } = await run(command, [
|
|
212
|
+
...extra,
|
|
213
|
+
'pr',
|
|
214
|
+
'create',
|
|
215
|
+
// No `--repo`: gh resolves the repository from the checkout it runs in, which is the checkout the
|
|
216
|
+
// request was written in. That is a mechanism, not a boundary — the container can edit that checkout,
|
|
217
|
+
// its remote included. What a deployment controls is which roots are served and which program runs;
|
|
218
|
+
// what this reaches is whatever the person running it can reach.
|
|
219
|
+
...(request.draft === true ? ['--draft'] : []),
|
|
220
|
+
'--base',
|
|
221
|
+
request.base,
|
|
222
|
+
'--head',
|
|
223
|
+
request.head,
|
|
224
|
+
'--title',
|
|
225
|
+
request.title,
|
|
226
|
+
'--body-file',
|
|
227
|
+
bodyFile,
|
|
228
|
+
], { cwd: repo, timeout: CREATE_TIMEOUT_MS, maxBuffer: 1 << 20, env: openerEnv(request.proxy) });
|
|
229
|
+
const found = pullRequestUrl(stdout);
|
|
230
|
+
if (!found) {
|
|
231
|
+
return { error: `no pull request URL in the output: ${(stdout.trim() || stderr.trim()).slice(0, 300)}` };
|
|
232
|
+
}
|
|
233
|
+
return found;
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
const failure = error;
|
|
237
|
+
const reason = (failure.stderr ?? failure.message ?? 'the command failed').trim();
|
|
238
|
+
return { error: reason.slice(0, 300) || 'the command failed' };
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=host-opener.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-opener.js","sourceRoot":"","sources":["../../src/commands/host-opener.ts"],"names":[],"mappings":"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"}
|