3.2 KB113 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import {ejeca} from 'shared/ejeca';
9import {Internal} from '../Internal';
10import {isEjecaError, isEjecaSpawnError} from '../utils';
11
12export default async function queryGraphQL<TData, TVariables>(
13 query: string,
14 variables: TVariables,
15 hostname: string,
16 timeoutMs?: number,
17): Promise<TData> {
18 if (Object.prototype.hasOwnProperty.call(variables, 'query')) {
19 throw Error('cannot have a variable named query');
20 }
21
22 const args = ['api', 'graphql'];
23 for (const [key, value] of Object.entries(variables as unknown as {[key: string]: unknown})) {
24 const type = typeof value;
25 switch (type) {
26 case 'boolean':
27 args.push('-F', `${key}=${value}`);
28 break;
29 case 'number':
30 args.push('-F', `${key}=${value}`);
31 break;
32 case 'string':
33 args.push('-f', `${key}=${value}`);
34 break;
35 default:
36 throw Error(`unexpected type: ${type} for ${key}: ${value}`);
37 }
38 }
39 args.push('--hostname', hostname);
40 args.push('-f', `query=${query}`);
41
42 let timedOut = false;
43
44 try {
45 const proc = ejeca('gh', args, {
46 env: {
47 ...((await Internal.additionalGhEnvVars?.()) ?? {}),
48 },
49 });
50
51 // TODO: move this into ejeca itself
52 let timeoutId: NodeJS.Timeout | undefined;
53 if (timeoutMs != null && timeoutMs > 0) {
54 timeoutId = setTimeout(() => {
55 proc.kill('SIGTERM', {forceKillAfterTimeout: 5_000});
56 timedOut = true;
57 }, timeoutMs);
58 proc.on('exit', () => {
59 clearTimeout(timeoutId);
60 });
61 }
62
63 const {stdout} = await proc;
64
65 const json = JSON.parse(stdout);
66
67 if (Array.isArray(json.errors)) {
68 return Promise.reject(`Error: ${json.errors[0].message}`);
69 }
70
71 return json.data;
72 } catch (error: unknown) {
73 if (isEjecaSpawnError(error)) {
74 if (error.code === 'ENOENT' || error.code === 'EACCES') {
75 // `gh` not installed on path
76 throw new Error(`GhNotInstalledError: ${(error as Error).stack}`);
77 }
78 } else if (isEjecaError(error)) {
79 // FIXME: we're never setting `code` in ejeca, so this is always false!
80 if (error.exitCode === 4) {
81 // `gh` CLI exit code 4 => authentication issue
82 throw new Error(`NotAuthenticatedError: ${(error as Error).stack}`);
83 }
84 }
85 if (timedOut) {
86 throw new Error(`TimedOutError: ${(error as Error).stack}`);
87 }
88 throw error;
89 }
90}
91
92/**
93 * Query `gh` CLI to test if a hostname is GitHub or GitHub Enterprise.
94 * Returns true if this hostname is a valid, authenticated GitHub instance.
95 * Returns false if the hostname is not github, or if you're not authenticated for that hostname,
96 * or if the network is not working.
97 */
98export async function isGithubEnterprise(hostname: string): Promise<boolean> {
99 const args = ['auth', 'status'];
100 args.push('--hostname', hostname);
101
102 try {
103 await ejeca('gh', args, {
104 env: {
105 ...((await Internal.additionalGhEnvVars?.()) ?? {}),
106 },
107 });
108 return true;
109 } catch {
110 return false;
111 }
112}
113