@google-cloud/cloud-run-mcp 1.2.0 → 1.5.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/.github/images/deploy_from_apps.gif +0 -0
- package/.github/images/deploy_from_ide.gif +0 -0
- package/.github/images/deploycli.gif +0 -0
- package/.github/workflows/local-tests.yml +1 -1
- package/CONTRIBUTING.md +8 -0
- package/README.md +22 -17
- package/gemini-extension/{Gemini.md → GEMINI.md} +2 -2
- package/gemini-extension.json +1 -1
- package/lib/cloud-run-deploy.js +162 -215
- package/lib/cloud-run-services.js +16 -4
- package/lib/gcp-api-helpers.js +143 -0
- package/lib/gcp-projects.js +1 -1
- package/mcp-server.js +8 -1
- package/package.json +3 -3
- package/test/local/gemini-extension.test.js +35 -0
- package/test/local/mcp-server.test.js +0 -1
- package/test/local/notifications.test.js +45 -0
- package/test/local/npx.test.js +36 -0
- package/test/local/tools.test.js +40 -65
- package/test/need-gcp/cloud-run-deploy.test.js +101 -135
- package/test/need-gcp/cloud-run-services.test.js +57 -77
- package/test/need-gcp/gcp-projects.test.js +14 -56
- package/{tools.js → tools/register-tools.js} +95 -461
- package/tools/tools.js +55 -0
package/lib/cloud-run-deploy.js
CHANGED
|
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { callWithRetry, ensureApisEnabled } from './gcp-api-helpers.js';
|
|
18
|
+
|
|
17
19
|
// Configuration
|
|
18
20
|
const REPO_NAME = 'mcp-cloud-run-deployments';
|
|
19
21
|
const ZIP_FILE_NAME = 'source.zip';
|
|
@@ -46,7 +48,7 @@ let runClient;
|
|
|
46
48
|
* @param {function(object): void} [progressCallback] - Optional callback for progress updates.
|
|
47
49
|
* @param {'debug' | 'info' | 'warn' | 'error'} [severity='info'] - The severity level of the message.
|
|
48
50
|
*/
|
|
49
|
-
function logAndProgress(message, progressCallback, severity = 'info') {
|
|
51
|
+
async function logAndProgress(message, progressCallback, severity = 'info') {
|
|
50
52
|
switch (severity) {
|
|
51
53
|
case 'error':
|
|
52
54
|
console.error(message);
|
|
@@ -63,91 +65,6 @@ function logAndProgress(message, progressCallback, severity = 'info') {
|
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
/**
|
|
67
|
-
* Checks if a single Google Cloud API is enabled and enables it if not.
|
|
68
|
-
*
|
|
69
|
-
* @param {object} serviceUsageClient - The Service Usage client.
|
|
70
|
-
* @param {string} serviceName - The full name of the service (e.g., 'projects/my-project/services/run.googleapis.com').
|
|
71
|
-
* @param {string} api - The API identifier (e.g., 'run.googleapis.com').
|
|
72
|
-
* @param {function(string, string=): void} progressCallback - A function to call with progress updates.
|
|
73
|
-
* @returns {Promise<void>} A promise that resolves when the API is enabled.
|
|
74
|
-
* @throws {Error} If the API fails to enable or if there's an issue checking its status.
|
|
75
|
-
*/
|
|
76
|
-
async function checkAndEnableApi(
|
|
77
|
-
serviceUsageClient,
|
|
78
|
-
serviceName,
|
|
79
|
-
api,
|
|
80
|
-
progressCallback
|
|
81
|
-
) {
|
|
82
|
-
const [service] = await serviceUsageClient.getService({
|
|
83
|
-
name: serviceName,
|
|
84
|
-
});
|
|
85
|
-
if (service.state !== 'ENABLED') {
|
|
86
|
-
logAndProgress(
|
|
87
|
-
`API [${api}] is not enabled. Enabling...`,
|
|
88
|
-
progressCallback
|
|
89
|
-
);
|
|
90
|
-
const [operation] = await serviceUsageClient.enableService({
|
|
91
|
-
name: serviceName,
|
|
92
|
-
});
|
|
93
|
-
await operation.promise();
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Ensures that the specified Google Cloud APIs are enabled for the given project.
|
|
99
|
-
* If an API is not enabled, it attempts to enable it. Retries any failure once after 1s.
|
|
100
|
-
* Throws an error if an API cannot be enabled.
|
|
101
|
-
*
|
|
102
|
-
* @async
|
|
103
|
-
* @param {string} projectId - The Google Cloud project ID.
|
|
104
|
-
* @param {string[]} apis - An array of API identifiers to check and enable (e.g., 'run.googleapis.com').
|
|
105
|
-
* @param {function(string, string=): void} progressCallback - A function to call with progress updates.
|
|
106
|
-
* The first argument is the message, the optional second argument is the type ('error', 'warning', 'info').
|
|
107
|
-
* @throws {Error} If an API fails to enable or if there's an issue checking its status.
|
|
108
|
-
* @returns {Promise<void>} A promise that resolves when all specified APIs are enabled.
|
|
109
|
-
*/
|
|
110
|
-
async function ensureApisEnabled(projectId, apis, progressCallback) {
|
|
111
|
-
const { ServiceUsageClient } = await import('@google-cloud/service-usage');
|
|
112
|
-
const serviceUsageClient = new ServiceUsageClient({ projectId });
|
|
113
|
-
logAndProgress('Checking and enabling required APIs...', progressCallback);
|
|
114
|
-
|
|
115
|
-
for (const api of apis) {
|
|
116
|
-
const serviceName = `projects/${projectId}/services/${api}`;
|
|
117
|
-
try {
|
|
118
|
-
await checkAndEnableApi(
|
|
119
|
-
serviceUsageClient,
|
|
120
|
-
serviceName,
|
|
121
|
-
api,
|
|
122
|
-
progressCallback
|
|
123
|
-
);
|
|
124
|
-
} catch (error) {
|
|
125
|
-
// First attempt failed, log a warning and retry once after a delay.
|
|
126
|
-
logAndProgress(
|
|
127
|
-
`Failed to check/enable ${api}, retrying in 1s...`,
|
|
128
|
-
progressCallback,
|
|
129
|
-
'warn'
|
|
130
|
-
);
|
|
131
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
132
|
-
try {
|
|
133
|
-
await checkAndEnableApi(
|
|
134
|
-
serviceUsageClient,
|
|
135
|
-
serviceName,
|
|
136
|
-
api,
|
|
137
|
-
progressCallback
|
|
138
|
-
);
|
|
139
|
-
} catch (retryError) {
|
|
140
|
-
// If the retry also fails, throw an error.
|
|
141
|
-
const errorMessage = `Failed to ensure API [${api}] is enabled after retry. Please check manually.`;
|
|
142
|
-
console.error(errorMessage, retryError);
|
|
143
|
-
logAndProgress(errorMessage, progressCallback, 'error');
|
|
144
|
-
throw new Error(errorMessage);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
logAndProgress('All required APIs are enabled.', progressCallback);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
68
|
/**
|
|
152
69
|
* Checks if a Cloud Run service already exists.
|
|
153
70
|
*
|
|
@@ -168,15 +85,18 @@ async function checkCloudRunServiceExists(
|
|
|
168
85
|
const parent = runClient.locationPath(projectId, location);
|
|
169
86
|
const servicePath = runClient.servicePath(projectId, location, serviceId);
|
|
170
87
|
try {
|
|
171
|
-
await
|
|
172
|
-
|
|
88
|
+
await callWithRetry(
|
|
89
|
+
() => runClient.getService({ name: servicePath }),
|
|
90
|
+
`getService ${serviceId}`
|
|
91
|
+
);
|
|
92
|
+
await logAndProgress(
|
|
173
93
|
`Cloud Run service ${serviceId} already exists.`,
|
|
174
94
|
progressCallback
|
|
175
95
|
);
|
|
176
96
|
return true;
|
|
177
97
|
} catch (error) {
|
|
178
98
|
if (error.code === 5) {
|
|
179
|
-
logAndProgress(
|
|
99
|
+
await logAndProgress(
|
|
180
100
|
`Cloud Run service ${serviceId} does not exist.`,
|
|
181
101
|
progressCallback
|
|
182
102
|
);
|
|
@@ -184,7 +104,7 @@ async function checkCloudRunServiceExists(
|
|
|
184
104
|
}
|
|
185
105
|
const errorMessage = `Error checking Cloud Run service ${serviceId}: ${error.message}`;
|
|
186
106
|
console.error(`Error checking Cloud Run service ${serviceId}:`, error);
|
|
187
|
-
logAndProgress(errorMessage, progressCallback, 'error');
|
|
107
|
+
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
188
108
|
throw error;
|
|
189
109
|
}
|
|
190
110
|
}
|
|
@@ -240,7 +160,7 @@ async function deployToCloudRun(
|
|
|
240
160
|
|
|
241
161
|
// Always perform a dry run first for general validation
|
|
242
162
|
try {
|
|
243
|
-
logAndProgress(
|
|
163
|
+
await logAndProgress(
|
|
244
164
|
`Performing dry run for service ${serviceId}...`,
|
|
245
165
|
progressCallback,
|
|
246
166
|
'debug'
|
|
@@ -249,25 +169,33 @@ async function deployToCloudRun(
|
|
|
249
169
|
|
|
250
170
|
if (exists) {
|
|
251
171
|
dryRunServiceConfig.name = servicePath;
|
|
252
|
-
await
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
172
|
+
await callWithRetry(
|
|
173
|
+
() =>
|
|
174
|
+
runClient.updateService({
|
|
175
|
+
service: dryRunServiceConfig,
|
|
176
|
+
validateOnly: true,
|
|
177
|
+
}),
|
|
178
|
+
`updateService (dry run) ${serviceId}`
|
|
179
|
+
);
|
|
256
180
|
} else {
|
|
257
|
-
await
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
181
|
+
await callWithRetry(
|
|
182
|
+
() =>
|
|
183
|
+
runClient.createService({
|
|
184
|
+
parent: parent,
|
|
185
|
+
service: dryRunServiceConfig,
|
|
186
|
+
serviceId: serviceId,
|
|
187
|
+
validateOnly: true,
|
|
188
|
+
}),
|
|
189
|
+
`createService (dry run) ${serviceId}`
|
|
190
|
+
);
|
|
263
191
|
}
|
|
264
|
-
logAndProgress(
|
|
192
|
+
await logAndProgress(
|
|
265
193
|
`Dry run successful for ${serviceId} with current configuration.`,
|
|
266
194
|
progressCallback,
|
|
267
195
|
'debug'
|
|
268
196
|
);
|
|
269
197
|
} catch (dryRunError) {
|
|
270
|
-
logAndProgress(
|
|
198
|
+
await logAndProgress(
|
|
271
199
|
`Dry run for ${serviceId} failed: ${dryRunError.message}`,
|
|
272
200
|
progressCallback,
|
|
273
201
|
'warn'
|
|
@@ -281,7 +209,7 @@ async function deployToCloudRun(
|
|
|
281
209
|
dryRunError.message.toLowerCase().includes('iam policy violation') ||
|
|
282
210
|
dryRunError.code === 3) /* INVALID_ARGUMENT */
|
|
283
211
|
) {
|
|
284
|
-
logAndProgress(
|
|
212
|
+
await logAndProgress(
|
|
285
213
|
`Dry run suggests 'invokerIamDisabled' is not allowed or invalid. Attempting deployment without it.`,
|
|
286
214
|
progressCallback,
|
|
287
215
|
'warn'
|
|
@@ -290,32 +218,42 @@ async function deployToCloudRun(
|
|
|
290
218
|
} else {
|
|
291
219
|
// For any other validation errors, rethrow to stop the deployment
|
|
292
220
|
const errorMessage = `Dry run validation failed for service ${serviceId}: ${dryRunError.message}`;
|
|
293
|
-
logAndProgress(errorMessage, progressCallback, 'error');
|
|
221
|
+
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
294
222
|
throw new Error(errorMessage);
|
|
295
223
|
}
|
|
296
224
|
}
|
|
297
225
|
|
|
298
226
|
let operation;
|
|
299
227
|
if (exists) {
|
|
300
|
-
logAndProgress(
|
|
228
|
+
await logAndProgress(
|
|
301
229
|
`Updating existing service ${serviceId}...`,
|
|
302
230
|
progressCallback
|
|
303
231
|
);
|
|
304
232
|
service.name = servicePath;
|
|
305
|
-
[operation] = await
|
|
233
|
+
[operation] = await callWithRetry(
|
|
234
|
+
() => runClient.updateService({ service }),
|
|
235
|
+
`updateService ${serviceId}`
|
|
236
|
+
);
|
|
306
237
|
} else {
|
|
307
|
-
logAndProgress(`Creating new service ${serviceId}...`, progressCallback);
|
|
308
|
-
[operation] = await
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
238
|
+
await logAndProgress(`Creating new service ${serviceId}...`, progressCallback);
|
|
239
|
+
[operation] = await callWithRetry(
|
|
240
|
+
() =>
|
|
241
|
+
runClient.createService({
|
|
242
|
+
parent: parent,
|
|
243
|
+
service: service, // 'service' object might have invokerIamDisabled removed
|
|
244
|
+
serviceId: serviceId,
|
|
245
|
+
}),
|
|
246
|
+
`createService ${serviceId}`
|
|
247
|
+
);
|
|
313
248
|
}
|
|
314
249
|
|
|
315
|
-
logAndProgress(
|
|
250
|
+
await logAndProgress(
|
|
251
|
+
`Deploying ${serviceId} to Cloud Run...`,
|
|
252
|
+
progressCallback
|
|
253
|
+
);
|
|
316
254
|
const [response] = await operation.promise();
|
|
317
255
|
|
|
318
|
-
logAndProgress(
|
|
256
|
+
await logAndProgress(
|
|
319
257
|
`Service deployed/updated successfully: ${response.uri}`,
|
|
320
258
|
progressCallback
|
|
321
259
|
);
|
|
@@ -323,7 +261,7 @@ async function deployToCloudRun(
|
|
|
323
261
|
} catch (error) {
|
|
324
262
|
const errorMessage = `Error deploying/updating service ${serviceId}: ${error.message}`;
|
|
325
263
|
console.error(`Error deploying/updating service ${serviceId}:`, error);
|
|
326
|
-
logAndProgress(errorMessage, progressCallback, 'error');
|
|
264
|
+
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
327
265
|
throw error;
|
|
328
266
|
}
|
|
329
267
|
}
|
|
@@ -346,20 +284,27 @@ async function ensureStorageBucketExists(
|
|
|
346
284
|
) {
|
|
347
285
|
const bucket = storage.bucket(bucketName);
|
|
348
286
|
try {
|
|
349
|
-
const [exists] = await
|
|
287
|
+
const [exists] = await callWithRetry(
|
|
288
|
+
() => bucket.exists(),
|
|
289
|
+
`storage.bucket.exists ${bucketName}`
|
|
290
|
+
);
|
|
350
291
|
if (exists) {
|
|
351
|
-
logAndProgress(
|
|
292
|
+
await logAndProgress(
|
|
293
|
+
`Bucket ${bucketName} already exists.`,
|
|
294
|
+
progressCallback
|
|
295
|
+
);
|
|
352
296
|
return bucket;
|
|
353
297
|
} else {
|
|
354
|
-
logAndProgress(
|
|
298
|
+
await logAndProgress(
|
|
355
299
|
`Bucket ${bucketName} does not exist. Creating in location ${location}...`,
|
|
356
300
|
progressCallback
|
|
357
301
|
);
|
|
358
302
|
try {
|
|
359
|
-
const [createdBucket] = await
|
|
360
|
-
location: location,
|
|
361
|
-
|
|
362
|
-
|
|
303
|
+
const [createdBucket] = await callWithRetry(
|
|
304
|
+
() => storage.createBucket(bucketName, { location: location }),
|
|
305
|
+
`storage.createBucket ${bucketName}`
|
|
306
|
+
);
|
|
307
|
+
await logAndProgress(
|
|
363
308
|
`Storage bucket ${createdBucket.name} created successfully in ${location}.`,
|
|
364
309
|
progressCallback
|
|
365
310
|
);
|
|
@@ -370,14 +315,14 @@ async function ensureStorageBucketExists(
|
|
|
370
315
|
`Failed to create storage bucket ${bucketName}. Error details:`,
|
|
371
316
|
createError
|
|
372
317
|
);
|
|
373
|
-
logAndProgress(errorMessage, progressCallback, 'error');
|
|
318
|
+
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
374
319
|
throw createError;
|
|
375
320
|
}
|
|
376
321
|
}
|
|
377
322
|
} catch (error) {
|
|
378
323
|
const errorMessage = `Error checking/creating bucket ${bucketName}: ${error.message}`;
|
|
379
324
|
console.error(`Error checking/creating bucket ${bucketName}:`, error);
|
|
380
|
-
logAndProgress(errorMessage, progressCallback, 'error');
|
|
325
|
+
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
381
326
|
throw error;
|
|
382
327
|
}
|
|
383
328
|
}
|
|
@@ -478,12 +423,15 @@ async function uploadToStorageBucket(
|
|
|
478
423
|
progressCallback
|
|
479
424
|
) {
|
|
480
425
|
try {
|
|
481
|
-
logAndProgress(
|
|
426
|
+
await logAndProgress(
|
|
482
427
|
`Uploading buffer to gs://${bucket.name}/${destinationBlobName}...`,
|
|
483
428
|
progressCallback
|
|
484
429
|
);
|
|
485
|
-
await
|
|
486
|
-
|
|
430
|
+
await callWithRetry(
|
|
431
|
+
() => bucket.file(destinationBlobName).save(buffer),
|
|
432
|
+
`storage.bucket.file.save ${destinationBlobName}`
|
|
433
|
+
);
|
|
434
|
+
await logAndProgress(
|
|
487
435
|
`File ${destinationBlobName} uploaded successfully to gs://${bucket.name}/${destinationBlobName}.`,
|
|
488
436
|
progressCallback
|
|
489
437
|
);
|
|
@@ -491,7 +439,7 @@ async function uploadToStorageBucket(
|
|
|
491
439
|
} catch (error) {
|
|
492
440
|
const errorMessage = `Error uploading buffer: ${error.message}`;
|
|
493
441
|
console.error(`Error uploading buffer:`, error);
|
|
494
|
-
logAndProgress(errorMessage, progressCallback, 'error');
|
|
442
|
+
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
495
443
|
throw error;
|
|
496
444
|
}
|
|
497
445
|
}
|
|
@@ -524,70 +472,51 @@ async function ensureArtifactRegistryRepoExists(
|
|
|
524
472
|
);
|
|
525
473
|
|
|
526
474
|
try {
|
|
527
|
-
const [repository] = await
|
|
528
|
-
name: repoPath,
|
|
529
|
-
|
|
530
|
-
|
|
475
|
+
const [repository] = await callWithRetry(
|
|
476
|
+
() => artifactRegistryClient.getRepository({ name: repoPath }),
|
|
477
|
+
`artifactRegistry.getRepository ${repositoryId}`
|
|
478
|
+
);
|
|
479
|
+
await logAndProgress(
|
|
531
480
|
`Repository ${repositoryId} already exists in ${location}.`,
|
|
532
481
|
progressCallback
|
|
533
482
|
);
|
|
534
483
|
return repository;
|
|
535
484
|
} catch (error) {
|
|
536
485
|
if (error.code === 5) {
|
|
537
|
-
logAndProgress(
|
|
486
|
+
await logAndProgress(
|
|
538
487
|
`Repository ${repositoryId} does not exist in ${location}. Creating...`,
|
|
539
488
|
progressCallback
|
|
540
489
|
);
|
|
541
490
|
const repositoryToCreate = {
|
|
542
491
|
format: format,
|
|
543
492
|
};
|
|
544
|
-
const maxRetries = 3;
|
|
545
|
-
const retryDelay = 10000; // 10s
|
|
546
493
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
494
|
+
const [operation] = await callWithRetry(
|
|
495
|
+
() =>
|
|
496
|
+
artifactRegistryClient.createRepository({
|
|
550
497
|
parent: parent,
|
|
551
498
|
repository: repositoryToCreate,
|
|
552
499
|
repositoryId: repositoryId,
|
|
553
|
-
})
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (createError.code === 7 && attempt < maxRetries) {
|
|
567
|
-
logAndProgress(
|
|
568
|
-
`Attempt ${attempt} to create repository failed. This can happen on new projects. Retrying in 10s...`,
|
|
569
|
-
progressCallback,
|
|
570
|
-
'warn'
|
|
571
|
-
);
|
|
572
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
573
|
-
} else {
|
|
574
|
-
const errorMessage = `Failed to create Artifact Registry repository ${repositoryId}. Error details: ${createError.message}`;
|
|
575
|
-
console.error(
|
|
576
|
-
`Failed to create Artifact Registry repository ${repositoryId}. Error details:`,
|
|
577
|
-
createError
|
|
578
|
-
);
|
|
579
|
-
logAndProgress(errorMessage, progressCallback, 'error');
|
|
580
|
-
throw createError;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
500
|
+
}),
|
|
501
|
+
`artifactRegistry.createRepository ${repositoryId}`
|
|
502
|
+
);
|
|
503
|
+
await logAndProgress(
|
|
504
|
+
`Creating Artifact Registry repository ${repositoryId}...`,
|
|
505
|
+
progressCallback
|
|
506
|
+
);
|
|
507
|
+
const [result] = await operation.promise();
|
|
508
|
+
await logAndProgress(
|
|
509
|
+
`Artifact Registry repository ${result.name} created successfully.`,
|
|
510
|
+
progressCallback
|
|
511
|
+
);
|
|
512
|
+
return result;
|
|
584
513
|
} else {
|
|
585
514
|
const errorMessage = `Error checking/creating repository ${repositoryId}: ${error.message}`;
|
|
586
515
|
console.error(
|
|
587
516
|
`Error checking/creating repository ${repositoryId}:`,
|
|
588
517
|
error
|
|
589
518
|
);
|
|
590
|
-
logAndProgress(errorMessage, progressCallback, 'error');
|
|
519
|
+
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
591
520
|
throw error;
|
|
592
521
|
}
|
|
593
522
|
}
|
|
@@ -657,23 +586,23 @@ async function triggerCloudBuild(
|
|
|
657
586
|
images: [targetImageUrl],
|
|
658
587
|
};
|
|
659
588
|
|
|
660
|
-
logAndProgress(
|
|
589
|
+
await logAndProgress(
|
|
661
590
|
`Initiating Cloud Build for gs://${sourceBucketName}/${sourceBlobName} in ${location}...`,
|
|
662
591
|
progressCallback
|
|
663
592
|
);
|
|
664
|
-
const [operation] = await
|
|
665
|
-
projectId: projectId,
|
|
666
|
-
|
|
667
|
-
|
|
593
|
+
const [operation] = await callWithRetry(
|
|
594
|
+
() => cloudBuildClient.createBuild({ projectId: projectId, build: build }),
|
|
595
|
+
'cloudBuild.createBuild'
|
|
596
|
+
);
|
|
668
597
|
|
|
669
|
-
logAndProgress(`Cloud Build job started...`, progressCallback);
|
|
598
|
+
await logAndProgress(`Cloud Build job started...`, progressCallback);
|
|
670
599
|
const buildId = operation.metadata.build.id;
|
|
671
600
|
let completedBuild;
|
|
672
601
|
while (true) {
|
|
673
|
-
const [getBuildOperation] = await
|
|
674
|
-
projectId: projectId,
|
|
675
|
-
|
|
676
|
-
|
|
602
|
+
const [getBuildOperation] = await callWithRetry(
|
|
603
|
+
() => cloudBuildClient.getBuild({ projectId: projectId, id: buildId }),
|
|
604
|
+
`cloudBuild.getBuild ${buildId}`
|
|
605
|
+
);
|
|
677
606
|
if (
|
|
678
607
|
['SUCCESS', 'FAILURE', 'INTERNAL_ERROR', 'TIMEOUT', 'CANCELLED'].includes(
|
|
679
608
|
getBuildOperation.status
|
|
@@ -682,7 +611,7 @@ async function triggerCloudBuild(
|
|
|
682
611
|
completedBuild = getBuildOperation;
|
|
683
612
|
break;
|
|
684
613
|
}
|
|
685
|
-
logAndProgress(
|
|
614
|
+
await logAndProgress(
|
|
686
615
|
`Build status: ${getBuildOperation.status}. Waiting...`,
|
|
687
616
|
progressCallback,
|
|
688
617
|
'debug'
|
|
@@ -691,20 +620,20 @@ async function triggerCloudBuild(
|
|
|
691
620
|
}
|
|
692
621
|
|
|
693
622
|
if (completedBuild.status === 'SUCCESS') {
|
|
694
|
-
logAndProgress(
|
|
623
|
+
await logAndProgress(
|
|
695
624
|
`Cloud Build job ${buildId} completed successfully.`,
|
|
696
625
|
progressCallback
|
|
697
626
|
);
|
|
698
|
-
logAndProgress(
|
|
627
|
+
await logAndProgress(
|
|
699
628
|
`Image built: ${completedBuild.results.images[0].name}`,
|
|
700
629
|
progressCallback
|
|
701
630
|
);
|
|
702
631
|
return completedBuild;
|
|
703
632
|
} else {
|
|
704
633
|
const failureMessage = `Cloud Build job ${buildId} failed with status: ${completedBuild.status}`;
|
|
705
|
-
logAndProgress(failureMessage, progressCallback, 'error');
|
|
634
|
+
await logAndProgress(failureMessage, progressCallback, 'error');
|
|
706
635
|
const logsMessage = `Build logs: ${completedBuild.logUrl}`;
|
|
707
|
-
logAndProgress(logsMessage, progressCallback); // Log URL is info, failure is error
|
|
636
|
+
await logAndProgress(logsMessage, progressCallback); // Log URL is info, failure is error
|
|
708
637
|
|
|
709
638
|
let buildLogsSnippet = `\n\nRefer to Log URL for full details: ${completedBuild.logUrl}`; // Default snippet
|
|
710
639
|
try {
|
|
@@ -712,7 +641,7 @@ async function triggerCloudBuild(
|
|
|
712
641
|
const loggingClient = new Logging({ projectId });
|
|
713
642
|
|
|
714
643
|
const logFilter = `resource.type="build" AND resource.labels.build_id="${buildId}"`;
|
|
715
|
-
logAndProgress(
|
|
644
|
+
await logAndProgress(
|
|
716
645
|
`Attempting to fetch last ${BUILD_LOGS_LINES_TO_FETCH} log lines for build ${buildId}...`,
|
|
717
646
|
progressCallback,
|
|
718
647
|
'debug'
|
|
@@ -724,25 +653,29 @@ async function triggerCloudBuild(
|
|
|
724
653
|
);
|
|
725
654
|
|
|
726
655
|
// Fetch the most recent N log entries
|
|
727
|
-
const [entries] = await
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
656
|
+
const [entries] = await callWithRetry(
|
|
657
|
+
() =>
|
|
658
|
+
loggingClient.getEntries({
|
|
659
|
+
filter: logFilter,
|
|
660
|
+
orderBy: 'timestamp desc', // Get latest logs first
|
|
661
|
+
pageSize: BUILD_LOGS_LINES_TO_FETCH,
|
|
662
|
+
}),
|
|
663
|
+
`logging.getEntries for build ${buildId}`
|
|
664
|
+
);
|
|
732
665
|
|
|
733
666
|
if (entries && entries.length > 0) {
|
|
734
667
|
// Entries are newest first, reverse for chronological order of the snippet
|
|
735
668
|
const logLines = entries.reverse().map((entry) => entry.data || '');
|
|
736
669
|
if (logLines.length > 0) {
|
|
737
670
|
buildLogsSnippet = `\n\nLast ${logLines.length} log lines from build ${buildId}:\n${logLines.join('\n')}`;
|
|
738
|
-
logAndProgress(
|
|
671
|
+
await logAndProgress(
|
|
739
672
|
`Successfully fetched snippet of build logs for ${buildId}.`,
|
|
740
673
|
progressCallback,
|
|
741
674
|
'info'
|
|
742
675
|
);
|
|
743
676
|
}
|
|
744
677
|
} else {
|
|
745
|
-
logAndProgress(
|
|
678
|
+
await logAndProgress(
|
|
746
679
|
`No specific log entries retrieved for build ${buildId}. ${buildLogsSnippet}`,
|
|
747
680
|
progressCallback,
|
|
748
681
|
'warn'
|
|
@@ -750,7 +683,7 @@ async function triggerCloudBuild(
|
|
|
750
683
|
}
|
|
751
684
|
} catch (logError) {
|
|
752
685
|
console.error(`Error fetching build logs for ${buildId}:`, logError);
|
|
753
|
-
logAndProgress(
|
|
686
|
+
await logAndProgress(
|
|
754
687
|
`Failed to fetch build logs snippet: ${logError.message}. ${buildLogsSnippet}`,
|
|
755
688
|
progressCallback,
|
|
756
689
|
'warn'
|
|
@@ -783,14 +716,21 @@ export async function deploy({
|
|
|
783
716
|
if (!projectId) {
|
|
784
717
|
const errorMsg =
|
|
785
718
|
'Error: projectId is required in the configuration object.';
|
|
786
|
-
logAndProgress(errorMsg, progressCallback, 'error');
|
|
787
|
-
|
|
719
|
+
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
720
|
+
throw new Error(errorMsg);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (!serviceName) {
|
|
724
|
+
const errorMsg =
|
|
725
|
+
'Error: serviceName is required in the configuration object.';
|
|
726
|
+
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
727
|
+
throw new Error(errorMsg);
|
|
788
728
|
}
|
|
789
729
|
|
|
790
730
|
if (!files || !Array.isArray(files) || files.length === 0) {
|
|
791
731
|
const errorMsg =
|
|
792
732
|
'Error: files array is required in the configuration object.';
|
|
793
|
-
logAndProgress(errorMsg, progressCallback, 'error');
|
|
733
|
+
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
794
734
|
if (typeof process !== 'undefined' && process.exit) {
|
|
795
735
|
process.exit(1);
|
|
796
736
|
} else {
|
|
@@ -823,10 +763,10 @@ export async function deploy({
|
|
|
823
763
|
const bucketName = `${projectId}-source-bucket`;
|
|
824
764
|
const imageUrl = `${region}-docker.pkg.dev/${projectId}/${REPO_NAME}/${serviceName}:${IMAGE_TAG}`;
|
|
825
765
|
|
|
826
|
-
logAndProgress(`Project: ${projectId}`, progressCallback);
|
|
827
|
-
logAndProgress(`Region: ${region}`, progressCallback);
|
|
828
|
-
logAndProgress(`Service Name: ${serviceName}`, progressCallback);
|
|
829
|
-
logAndProgress(`Files to deploy: ${files.length}`, progressCallback);
|
|
766
|
+
await logAndProgress(`Project: ${projectId}`, progressCallback);
|
|
767
|
+
await logAndProgress(`Region: ${region}`, progressCallback);
|
|
768
|
+
await logAndProgress(`Service Name: ${serviceName}`, progressCallback);
|
|
769
|
+
await logAndProgress(`Files to deploy: ${files.length}`, progressCallback);
|
|
830
770
|
|
|
831
771
|
let hasDockerfile = false;
|
|
832
772
|
if (
|
|
@@ -859,7 +799,7 @@ export async function deploy({
|
|
|
859
799
|
}
|
|
860
800
|
}
|
|
861
801
|
}
|
|
862
|
-
logAndProgress(`Dockerfile: ${hasDockerfile}`, progressCallback);
|
|
802
|
+
await logAndProgress(`Dockerfile: ${hasDockerfile}`, progressCallback);
|
|
863
803
|
|
|
864
804
|
const bucket = await ensureStorageBucketExists(
|
|
865
805
|
bucketName,
|
|
@@ -874,7 +814,7 @@ export async function deploy({
|
|
|
874
814
|
ZIP_FILE_NAME,
|
|
875
815
|
progressCallback
|
|
876
816
|
);
|
|
877
|
-
logAndProgress('Source code uploaded successfully', progressCallback);
|
|
817
|
+
await logAndProgress('Source code uploaded successfully', progressCallback);
|
|
878
818
|
|
|
879
819
|
await ensureArtifactRegistryRepoExists(
|
|
880
820
|
projectId,
|
|
@@ -906,12 +846,12 @@ export async function deploy({
|
|
|
906
846
|
skipIamCheck
|
|
907
847
|
);
|
|
908
848
|
|
|
909
|
-
logAndProgress(`Deployment Completed Successfully`, progressCallback);
|
|
849
|
+
await logAndProgress(`Deployment Completed Successfully`, progressCallback);
|
|
910
850
|
return service;
|
|
911
851
|
} catch (error) {
|
|
912
852
|
const deployFailedMessage = `Deployment Failed: ${error.message}`;
|
|
913
853
|
console.error(`Deployment Failed`, error);
|
|
914
|
-
logAndProgress(deployFailedMessage, progressCallback, 'error');
|
|
854
|
+
await logAndProgress(deployFailedMessage, progressCallback, 'error');
|
|
915
855
|
throw error;
|
|
916
856
|
}
|
|
917
857
|
}
|
|
@@ -939,13 +879,20 @@ export async function deployImage({
|
|
|
939
879
|
if (!projectId) {
|
|
940
880
|
const errorMsg =
|
|
941
881
|
'Error: projectId is required in the configuration object.';
|
|
942
|
-
logAndProgress(errorMsg, progressCallback, 'error');
|
|
943
|
-
|
|
882
|
+
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
883
|
+
throw new Error(errorMsg);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (!serviceName) {
|
|
887
|
+
const errorMsg =
|
|
888
|
+
'Error: serviceName is required in the configuration object.';
|
|
889
|
+
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
890
|
+
throw new Error(errorMsg);
|
|
944
891
|
}
|
|
945
892
|
|
|
946
893
|
if (!imageUrl) {
|
|
947
894
|
const errorMsg = 'Error: imageUrl is required in the configuration object.';
|
|
948
|
-
logAndProgress(errorMsg, progressCallback, 'error');
|
|
895
|
+
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
949
896
|
if (typeof process !== 'undefined' && process.exit) {
|
|
950
897
|
process.exit(1);
|
|
951
898
|
} else {
|
|
@@ -965,10 +912,10 @@ export async function deployImage({
|
|
|
965
912
|
|
|
966
913
|
runClient = new ServicesClient({ projectId });
|
|
967
914
|
|
|
968
|
-
logAndProgress(`Project: ${projectId}`, progressCallback);
|
|
969
|
-
logAndProgress(`Region: ${region}`, progressCallback);
|
|
970
|
-
logAndProgress(`Service Name: ${serviceName}`, progressCallback);
|
|
971
|
-
logAndProgress(`Image URL: ${imageUrl}`, progressCallback);
|
|
915
|
+
await logAndProgress(`Project: ${projectId}`, progressCallback);
|
|
916
|
+
await logAndProgress(`Region: ${region}`, progressCallback);
|
|
917
|
+
await logAndProgress(`Service Name: ${serviceName}`, progressCallback);
|
|
918
|
+
await logAndProgress(`Image URL: ${imageUrl}`, progressCallback);
|
|
972
919
|
|
|
973
920
|
const service = await deployToCloudRun(
|
|
974
921
|
projectId,
|
|
@@ -979,12 +926,12 @@ export async function deployImage({
|
|
|
979
926
|
skipIamCheck
|
|
980
927
|
);
|
|
981
928
|
|
|
982
|
-
logAndProgress(`Deployment Completed Successfully`, progressCallback);
|
|
929
|
+
await logAndProgress(`Deployment Completed Successfully`, progressCallback);
|
|
983
930
|
return service;
|
|
984
931
|
} catch (error) {
|
|
985
932
|
const deployFailedMessage = `Deployment Failed: ${error.message}`;
|
|
986
933
|
console.error(`Deployment Failed`, error);
|
|
987
|
-
logAndProgress(deployFailedMessage, progressCallback, 'error');
|
|
934
|
+
await logAndProgress(deployFailedMessage, progressCallback, 'error');
|
|
988
935
|
throw error;
|
|
989
936
|
}
|
|
990
937
|
}
|