-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathindex.ts
More file actions
347 lines (313 loc) · 10.8 KB
/
index.ts
File metadata and controls
347 lines (313 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as path from 'path';
import * as os from 'os';
import { ExecOptions as ActionsExecOptions } from '@actions/exec/lib/interfaces';
import { getExecOutput } from '@actions/exec';
import { HttpClient } from '@actions/http-client';
import * as core from '@actions/core';
import * as toolCache from '@actions/tool-cache';
import * as semver from 'semver';
import { errorMessage, withRetries } from '@google-github-actions/actions-utils';
import { buildReleaseURL } from './format-url';
import { downloadAndExtractTool } from './download-util';
// Do not listen to the linter - this can NOT be rewritten as an ES6 import statement.
const { version: appVersion } = require('../package.json');
// versionsURL is the URL to the artifact where version information is stored.
const versionsURL = `https://raw.githubusercontent.com/google-github-actions/setup-cloud-sdk/main/data/versions.json`;
/**
* userAgentString is the UA to use for this installation. It dynamically pulls
* the app version from the package declaration.
*/
export const userAgentString = `google-github-actions:setup-cloud-sdk/${appVersion}`;
/**
* Checks if gcloud is installed.
*
* @param version - (Optional) Cloud SDK version.
* @returns true if gcloud is found in toolpath.
*/
export function isInstalled(version?: string): boolean {
let toolPath;
if (version) {
toolPath = toolCache.find('gcloud', version);
return toolPath != undefined && toolPath !== '';
}
toolPath = toolCache.findAllVersions('gcloud');
return toolPath.length > 0;
}
/**
* Returns the correct gcloud command for OS.
*
* @returns gcloud command.
*/
export function getToolCommand(): string {
// A workaround for https://github.com/actions/toolkit/issues/229
// Currently exec on windows runs as cmd shell.
let toolCommand = 'gcloud';
if (process.platform == 'win32') {
toolCommand = 'gcloud.cmd';
}
return toolCommand;
}
/**
* ExecOptions is a type alias to core/exec ExecOptions.
*/
export type ExecOptions = ActionsExecOptions;
/**
* ExecOutput is the output returned from a gcloud exec.
*/
export type ExecOutput = {
stderr: string;
stdout: string;
output: string;
};
/**
* gcloudRun executes the given gcloud command using actions/exec under the
* hood. It handles non-zero exit codes and throws a more semantic error on
* failure.
*
* @param cmd The command to run.
* @param options Any options.
*
* @return ExecOutput
*/
export async function gcloudRun(cmd: string[], options?: ExecOptions): Promise<ExecOutput> {
const toolCommand = getToolCommand();
const opts = Object.assign({}, { silent: true, ignoreReturnCode: true }, options);
const commandString = `${toolCommand} ${cmd.join(' ')}`;
core.debug(`Running command: ${commandString}`);
const result = await getExecOutput(toolCommand, cmd, opts);
if (result.exitCode !== 0) {
const errMsg = result.stderr || `command exited ${result.exitCode}, but stderr had no output`;
throw new Error(`failed to execute command \`${commandString}\`: ${errMsg}`);
}
return {
stderr: result.stderr,
stdout: result.stdout,
output: result.stdout + '\n' + result.stderr,
};
}
/**
* gcloudRunJSON runs the gcloud command with JSON output and parses the result
* as JSON. If the parsing fails, it throws an error.
*
* @param cmd The command to run.
* @param options Any options.
*
* @return Parsed JSON as an object (or array).
*/
export async function gcloudRunJSON(cmd: string[], options?: ExecOptions): Promise<any> {
const jsonCmd = ['--format', 'json'].concat(cmd);
const output = await gcloudRun(jsonCmd, options);
try {
const parsed = JSON.parse(output.stdout);
return parsed;
} catch (err) {
throw new Error(
`failed to parse output as JSON: ${err}\n\nstdout:\n${output.stdout}\n\nstderr:\n${output.stderr}`,
);
}
}
/**
* Checks if the project Id is set in the gcloud config.
*
* @returns true is project Id is set.
*/
export async function isProjectIdSet(): Promise<boolean> {
const result = await gcloudRun(['config', 'get-value', 'project']);
return !result.output.includes('unset');
}
/**
* Checks if gcloud is authenticated.
*
* @returns true is gcloud is authenticated.
*/
export async function isAuthenticated(): Promise<boolean> {
const result = await gcloudRun(['auth', 'list']);
return !result.output.includes('No credentialed accounts.');
}
/**
* Installs the gcloud SDK into the actions environment.
*
* @param version - The version or version specification to install. If a
* specification is given, the most recent version that still matches the
* specification is installed.
* @returns The path of the installed tool.
*/
export async function installGcloudSDK(
version: string,
useToolCache: boolean = false,
): Promise<string> {
// Retrieve the release corresponding to the specified version and OS
const osPlat = os.platform();
const osArch = os.arch();
const resolvedVersion = toolCache.isExplicitVersion(version)
? version
: await bestVersion(version);
const url = buildReleaseURL(osPlat, osArch, resolvedVersion);
// Download and extract the release
const extPath = await downloadAndExtractTool(url);
if (!extPath) {
throw new Error(`Failed to download release, url: ${url}`);
}
// Either cache the tool or just add it directly to the path.
//
// Caching the tool on disk takes a really long time, and it's not clear
// whether it's even valuable since it's ONLY cached on disk on the runner.
// For GitHub-managed runners, that is useless since they are ephemeral.
//
// See https://github.com/google-github-actions/setup-gcloud/issues/701 for
// discussion, but it's actually faster to skip the caching and just add the
// tool directly to the path.
if (useToolCache) {
const toolRoot = path.join(extPath, 'google-cloud-sdk');
const cachedToolRoot = await toolCache.cacheDir(toolRoot, 'gcloud', resolvedVersion, osArch);
core.addPath(path.join(cachedToolRoot, 'bin'));
return cachedToolRoot;
} else {
const toolRoot = path.join(extPath, 'google-cloud-sdk');
core.addPath(path.join(toolRoot, 'bin'));
return toolRoot;
}
}
/**
* computeGcloudVersion computes the appropriate gcloud version for the given
* string. If the string is the empty string or the special value "latest", it
* returns the latest known version of the Google Cloud SDK. Otherwise it
* returns the provided string. It does not validate that the string is a valid
* version.
*
* This is most useful when accepting user input which should default to
* "latest" or the empty string when you want the latest version to be
* installed, but still want users to be able to choose a specific version to
* install as a customization.
*
* @deprecated Callers should use `installGcloudSDK('> 0.0.0.')` instead.
*
* @param version String (or undefined) version. The empty string or other
* falsey values will return the latest gcloud version.
*
* @return String representing the latest version.
*/
export async function computeGcloudVersion(version?: string): Promise<string> {
version = (version || '').trim();
if (version === '' || version === 'latest') {
return await getLatestGcloudSDKVersion();
}
return version;
}
/**
* Authenticates the gcloud tool using the provided credentials file.
*
* @param filepath - Path to the credentials file.
*/
export async function authenticateGcloudSDK(filepath: string): Promise<void> {
await gcloudRun(['--quiet', 'auth', 'login', '--force', '--cred-file', filepath]);
}
/**
* Sets the GCP Project Id in the gcloud config.
*
* @param projectId - The project ID to set.
* @returns project ID.
*/
export async function setProject(projectId: string): Promise<void> {
await gcloudRun(['--quiet', 'config', 'set', 'project', projectId]);
}
/**
* Install a Cloud SDK component.
*
* @param component - gcloud component group to install ie alpha, beta.
* @returns CMD output
*/
export async function installComponent(component: string[] | string): Promise<void> {
let cmd = ['--quiet', 'components', 'install'];
if (Array.isArray(component)) {
cmd = cmd.concat(component);
} else {
cmd.push(component);
}
await gcloudRun(cmd);
}
/**
* getLatestGcloudSDKVersion fetches the latest version number from the API.
*
* @returns The latest stable version of the gcloud SDK.
*/
export async function getLatestGcloudSDKVersion(): Promise<string> {
return await bestVersion('> 0.0.0');
}
/**
* bestVersion takes a version constraint and gets the latest available version
* that satisfies the constraint.
*
* @param spec Version specification
* @return Resolved version
*/
export async function bestVersion(spec: string): Promise<string> {
let versions: string[];
try {
return await withRetries(
async (): Promise<string> => {
const http = new HttpClient(userAgentString);
const res = await http.get(versionsURL);
const body = await res.readBody();
const statusCode = res.message.statusCode || 500;
if (statusCode >= 400) {
throw new Error(`(${statusCode}) ${body}`);
}
versions = JSON.parse(body) as string[];
return computeBestVersion(spec, versions);
},
{
retries: 3,
backoff: 100, // 100 milliseconds
backoffLimit: 1_000, // 1 second
},
)();
} catch (err) {
const msg = errorMessage(err);
throw new Error(`failed to retrieve versions from ${versionsURL}: ${msg}`);
}
}
/**
* computeBestVersion computes the latest available version that still satisfies
* the spec. This is a helper function and is only exported for testing.
*
* @param versions List of versions
* @param spec Version specification
*
* @return Best version or an error if no matches are found
*/
export function computeBestVersion(spec: string, versions: string[]): string {
// Sort all versions
versions = versions.sort((a, b) => {
return semver.gt(a, b) ? 1 : -1;
});
// Find the latest version that still satisfies the spec.
let resolved = '';
for (let i = versions.length - 1; i >= 0; i--) {
const candidate = versions[i];
if (semver.satisfies(candidate, spec)) {
resolved = candidate;
break;
}
}
if (!resolved) {
throw new Error(`failed to find any versions matching "${spec}"`);
}
return resolved;
}
export * from './test-util';