@google-cloud/cloud-run-mcp 1.5.0 → 1.6.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.
Files changed (37) hide show
  1. package/.c8rc.json +9 -0
  2. package/.github/workflows/lint-checker.yml +17 -0
  3. package/.kokoro/kokoro_build.sh +22 -0
  4. package/.kokoro/presubmit.cfg +21 -0
  5. package/.kokoro/run_tests.sh +31 -0
  6. package/CONTRIBUTING.md +23 -8
  7. package/README.md +52 -11
  8. package/REVIEWING.md +20 -0
  9. package/gemini-extension/GEMINI.md +1 -1
  10. package/lib/cloud-api/build.js +200 -0
  11. package/lib/{gcp-api-helpers.js → cloud-api/helpers.js} +12 -8
  12. package/lib/{gcp-projects.js → cloud-api/projects.js} +58 -28
  13. package/lib/cloud-api/registry.js +98 -0
  14. package/lib/{cloud-run-services.js → cloud-api/run.js} +103 -21
  15. package/lib/cloud-api/storage.js +123 -0
  16. package/lib/deployment/deployer.js +474 -0
  17. package/lib/util/archive.js +95 -0
  18. package/lib/util/helpers.js +42 -0
  19. package/mcp-server.js +21 -10
  20. package/package.json +7 -4
  21. package/test/local/cloud-api/build.test.js +436 -0
  22. package/test/local/cloud-api/projects.test.js +18 -0
  23. package/test/local/mcp-server-streamable-http.test.js +57 -0
  24. package/test/local/notifications.test.js +9 -6
  25. package/test/local/tools.test.js +114 -70
  26. package/test/need-gcp/cloud-run-services.test.js +44 -44
  27. package/test/need-gcp/{cloud-run-deploy.test.js → deployer.test.js} +3 -56
  28. package/test/need-gcp/gcp-auth-check.test.js +1 -1
  29. package/test/need-gcp/gcp-projects.test.js +3 -15
  30. package/test/need-gcp/test-helpers.js +175 -0
  31. package/test/need-gcp/workflows/deployment-workflows.test.js +100 -0
  32. package/tools/register-tools.js +106 -97
  33. package/tools/tools.js +2 -9
  34. package/lib/cloud-run-deploy.js +0 -937
  35. /package/lib/{gcp-auth-check.js → cloud-api/auth.js} +0 -0
  36. /package/lib/{gcp-billing-accounts.js → cloud-api/billing.js} +0 -0
  37. /package/lib/{gcp-metadata.js → cloud-api/metadata.js} +0 -0
@@ -1,937 +0,0 @@
1
- /*
2
- Copyright 2025 Google LLC
3
-
4
- Licensed under the Apache License, Version 2.0 (the "License");
5
- you may not use this file except in compliance with the License.
6
- You may obtain a copy of the License at
7
-
8
- https://www.apache.org/licenses/LICENSE-2.0
9
-
10
- Unless required by applicable law or agreed to in writing, software
11
- distributed under the License is distributed on an "AS IS" BASIS,
12
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- See the License for the specific language governing permissions and
14
- limitations under the License.
15
- */
16
-
17
- import { callWithRetry, ensureApisEnabled } from './gcp-api-helpers.js';
18
-
19
- // Configuration
20
- const REPO_NAME = 'mcp-cloud-run-deployments';
21
- const ZIP_FILE_NAME = 'source.zip';
22
- const IMAGE_TAG = 'latest';
23
-
24
- // APIs required for deploying from source code.
25
- const REQUIRED_APIS_FOR_SOURCE_DEPLOY = [
26
- 'iam.googleapis.com',
27
- 'storage.googleapis.com',
28
- 'cloudbuild.googleapis.com',
29
- 'artifactregistry.googleapis.com',
30
- 'run.googleapis.com',
31
- ];
32
-
33
- // APIs required for deploying a container image.
34
- const REQUIRED_APIS_FOR_IMAGE_DEPLOY = ['run.googleapis.com'];
35
-
36
- const DELAY_WAIT_FOR_BUILD_LOGS = 10000; // 10 seconds delay to allow logs to propagate
37
- const BUILD_LOGS_LINES_TO_FETCH = 100; // Number of log lines to fetch for build logs snippet
38
-
39
- // Initialize Clients
40
- let storage;
41
- let cloudBuildClient;
42
- let artifactRegistryClient;
43
- let runClient;
44
-
45
- /**
46
- * Helper function to log a message and call the progress callback.
47
- * @param {string} message - The message to log.
48
- * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
49
- * @param {'debug' | 'info' | 'warn' | 'error'} [severity='info'] - The severity level of the message.
50
- */
51
- async function logAndProgress(message, progressCallback, severity = 'info') {
52
- switch (severity) {
53
- case 'error':
54
- console.error(message);
55
- break;
56
- case 'warn':
57
- case 'info':
58
- case 'debug':
59
- default:
60
- console.log(message);
61
- break;
62
- }
63
- if (progressCallback) {
64
- progressCallback({ level: severity, data: message });
65
- }
66
- }
67
-
68
- /**
69
- * Checks if a Cloud Run service already exists.
70
- *
71
- * @async
72
- * @param {string} projectId - The Google Cloud project ID.
73
- * @param {string} location - The Google Cloud region where the service is located.
74
- * @param {string} serviceId - The ID of the Cloud Run service.
75
- * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
76
- * @returns {Promise<boolean>} A promise that resolves to true if the service exists, false otherwise.
77
- * @throws {Error} If there's an error checking the service (other than not found).
78
- */
79
- async function checkCloudRunServiceExists(
80
- projectId,
81
- location,
82
- serviceId,
83
- progressCallback
84
- ) {
85
- const parent = runClient.locationPath(projectId, location);
86
- const servicePath = runClient.servicePath(projectId, location, serviceId);
87
- try {
88
- await callWithRetry(
89
- () => runClient.getService({ name: servicePath }),
90
- `getService ${serviceId}`
91
- );
92
- await logAndProgress(
93
- `Cloud Run service ${serviceId} already exists.`,
94
- progressCallback
95
- );
96
- return true;
97
- } catch (error) {
98
- if (error.code === 5) {
99
- await logAndProgress(
100
- `Cloud Run service ${serviceId} does not exist.`,
101
- progressCallback
102
- );
103
- return false;
104
- }
105
- const errorMessage = `Error checking Cloud Run service ${serviceId}: ${error.message}`;
106
- console.error(`Error checking Cloud Run service ${serviceId}:`, error);
107
- await logAndProgress(errorMessage, progressCallback, 'error');
108
- throw error;
109
- }
110
- }
111
-
112
- /**
113
- * Deploys or updates a Cloud Run service with the specified container image.
114
- * If the service exists, it's updated; otherwise, a new service is created.
115
- * The service is configured to be publicly accessible.
116
- *
117
- * @async
118
- * @param {string} projectId - The Google Cloud project ID.
119
- * @param {string} location - The Google Cloud region for the deployment.
120
- * @param {string} serviceId - The ID for the Cloud Run service.
121
- * @param {string} imgUrl - The URL of the container image to deploy.
122
- * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
123
- * @returns {Promise<object>} A promise that resolves with the Cloud Run service object upon successful deployment or update.
124
- * @throws {Error} If the deployment or update process fails.
125
- */
126
- async function deployToCloudRun(
127
- projectId,
128
- location,
129
- serviceId,
130
- imgUrl,
131
- progressCallback,
132
- skipIamCheck
133
- ) {
134
- const parent = runClient.locationPath(projectId, location);
135
- const servicePath = runClient.servicePath(projectId, location, serviceId);
136
- const revisionName = `${serviceId}-${Date.now()}`; // Generate a unique revision name
137
-
138
- const service = {
139
- template: {
140
- revision: revisionName,
141
- containers: [{ image: imgUrl }],
142
- },
143
- labels: {
144
- 'created-by': 'cloud-run-mcp',
145
- },
146
- };
147
-
148
- // Conditionally set invokerIamDisabled based on the skipIamCheck flag
149
- if (skipIamCheck) {
150
- service.invokerIamDisabled = true;
151
- }
152
-
153
- try {
154
- const exists = await checkCloudRunServiceExists(
155
- projectId,
156
- location,
157
- serviceId,
158
- progressCallback
159
- );
160
-
161
- // Always perform a dry run first for general validation
162
- try {
163
- await logAndProgress(
164
- `Performing dry run for service ${serviceId}...`,
165
- progressCallback,
166
- 'debug'
167
- );
168
- const dryRunServiceConfig = JSON.parse(JSON.stringify(service)); // Deep copy for dry run
169
-
170
- if (exists) {
171
- dryRunServiceConfig.name = servicePath;
172
- await callWithRetry(
173
- () =>
174
- runClient.updateService({
175
- service: dryRunServiceConfig,
176
- validateOnly: true,
177
- }),
178
- `updateService (dry run) ${serviceId}`
179
- );
180
- } else {
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
- );
191
- }
192
- await logAndProgress(
193
- `Dry run successful for ${serviceId} with current configuration.`,
194
- progressCallback,
195
- 'debug'
196
- );
197
- } catch (dryRunError) {
198
- await logAndProgress(
199
- `Dry run for ${serviceId} failed: ${dryRunError.message}`,
200
- progressCallback,
201
- 'warn'
202
- );
203
-
204
- // Check if the error is related to invokerIamDisabled (this is a heuristic)
205
- if (
206
- skipIamCheck &&
207
- dryRunError.message &&
208
- (dryRunError.message.toLowerCase().includes('invokeriamdisabled') ||
209
- dryRunError.message.toLowerCase().includes('iam policy violation') ||
210
- dryRunError.code === 3) /* INVALID_ARGUMENT */
211
- ) {
212
- await logAndProgress(
213
- `Dry run suggests 'invokerIamDisabled' is not allowed or invalid. Attempting deployment without it.`,
214
- progressCallback,
215
- 'warn'
216
- );
217
- delete service.invokerIamDisabled; // Modify the main service object for actual deployment
218
- } else {
219
- // For any other validation errors, rethrow to stop the deployment
220
- const errorMessage = `Dry run validation failed for service ${serviceId}: ${dryRunError.message}`;
221
- await logAndProgress(errorMessage, progressCallback, 'error');
222
- throw new Error(errorMessage);
223
- }
224
- }
225
-
226
- let operation;
227
- if (exists) {
228
- await logAndProgress(
229
- `Updating existing service ${serviceId}...`,
230
- progressCallback
231
- );
232
- service.name = servicePath;
233
- [operation] = await callWithRetry(
234
- () => runClient.updateService({ service }),
235
- `updateService ${serviceId}`
236
- );
237
- } else {
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
- );
248
- }
249
-
250
- await logAndProgress(
251
- `Deploying ${serviceId} to Cloud Run...`,
252
- progressCallback
253
- );
254
- const [response] = await operation.promise();
255
-
256
- await logAndProgress(
257
- `Service deployed/updated successfully: ${response.uri}`,
258
- progressCallback
259
- );
260
- return response;
261
- } catch (error) {
262
- const errorMessage = `Error deploying/updating service ${serviceId}: ${error.message}`;
263
- console.error(`Error deploying/updating service ${serviceId}:`, error);
264
- await logAndProgress(errorMessage, progressCallback, 'error');
265
- throw error;
266
- }
267
- }
268
-
269
- /**
270
- * Ensures that a Google Cloud Storage bucket exists.
271
- * If the bucket does not exist, it attempts to create it in the specified location.
272
- *
273
- * @async
274
- * @param {string} bucketName - The name of the storage bucket.
275
- * @param {string} [location='us'] - The location to create the bucket in if it doesn't exist. Defaults to 'us'.
276
- * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
277
- * @returns {Promise<import('@google-cloud/storage').Bucket>} A promise that resolves with the GCS Bucket object.
278
- * @throws {Error} If there's an error checking or creating the bucket.
279
- */
280
- async function ensureStorageBucketExists(
281
- bucketName,
282
- location = 'us',
283
- progressCallback
284
- ) {
285
- const bucket = storage.bucket(bucketName);
286
- try {
287
- const [exists] = await callWithRetry(
288
- () => bucket.exists(),
289
- `storage.bucket.exists ${bucketName}`
290
- );
291
- if (exists) {
292
- await logAndProgress(
293
- `Bucket ${bucketName} already exists.`,
294
- progressCallback
295
- );
296
- return bucket;
297
- } else {
298
- await logAndProgress(
299
- `Bucket ${bucketName} does not exist. Creating in location ${location}...`,
300
- progressCallback
301
- );
302
- try {
303
- const [createdBucket] = await callWithRetry(
304
- () => storage.createBucket(bucketName, { location: location }),
305
- `storage.createBucket ${bucketName}`
306
- );
307
- await logAndProgress(
308
- `Storage bucket ${createdBucket.name} created successfully in ${location}.`,
309
- progressCallback
310
- );
311
- return createdBucket;
312
- } catch (createError) {
313
- const errorMessage = `Failed to create storage bucket ${bucketName}. Error details: ${createError.message}`;
314
- console.error(
315
- `Failed to create storage bucket ${bucketName}. Error details:`,
316
- createError
317
- );
318
- await logAndProgress(errorMessage, progressCallback, 'error');
319
- throw createError;
320
- }
321
- }
322
- } catch (error) {
323
- const errorMessage = `Error checking/creating bucket ${bucketName}: ${error.message}`;
324
- console.error(`Error checking/creating bucket ${bucketName}:`, error);
325
- await logAndProgress(errorMessage, progressCallback, 'error');
326
- throw error;
327
- }
328
- }
329
-
330
- /**
331
- * Creates a zip archive in memory from a list of file paths and/or file objects.
332
- * File objects should have `filename` (string) and `content` (Buffer or string) properties.
333
- *
334
- * @param {Array<string|{filename: string, content: Buffer|string}>} files - An array of items to zip.
335
- * Each item can be a string representing a file/directory path, or an object
336
- * with `filename` and `content` properties for in-memory files.
337
- * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
338
- * @returns {Promise<Buffer>} A promise that resolves with a Buffer containing the zip data.
339
- * @throws {Error} If an input file path is not found, an input item has an invalid format, or an archiver error occurs.
340
- */
341
- async function zipFiles(files, progressCallback) {
342
- const path = await import('path');
343
- const fs = await import('fs');
344
- const archiver = (await import('archiver')).default;
345
-
346
- return new Promise((resolve, reject) => {
347
- logAndProgress('Creating in-memory zip archive...', progressCallback);
348
- const chunks = [];
349
- const archive = archiver('zip', {
350
- zlib: { level: 9 },
351
- });
352
-
353
- archive.on('data', (chunk) => chunks.push(chunk));
354
- archive.on('end', () => {
355
- logAndProgress(
356
- `Files zipped successfully. Total size: ${archive.pointer()} bytes`,
357
- progressCallback
358
- );
359
- resolve(Buffer.concat(chunks));
360
- });
361
-
362
- archive.on('warning', (err) => {
363
- const warningMessage = `Archiver warning: ${err}`;
364
- logAndProgress(warningMessage, progressCallback, 'warn');
365
- if (err.code !== 'ENOENT') {
366
- // ENOENT is often just a warning, others might be more critical for zip
367
- reject(err);
368
- }
369
- });
370
-
371
- archive.on('error', (err) => {
372
- const errorMessage = `Archiver error: ${err.message}`;
373
- console.error(errorMessage, err);
374
- logAndProgress(errorMessage, progressCallback, 'error');
375
- reject(err);
376
- });
377
-
378
- files.forEach((file) => {
379
- if (typeof file === 'object' && 'filename' in file && 'content' in file) {
380
- archive.append(file.content, { name: file.filename });
381
- } else if (typeof file === 'string') {
382
- let pathInput = file;
383
-
384
- // This is a "hack" to better support WSL on Windows. AI agents tend to send path that start with '/c' in that case. Re-write it to '/mnt/c'
385
- if (pathInput.startsWith('/c')) {
386
- pathInput = `/mnt${pathInput}`;
387
- }
388
- const filePath = path.resolve(pathInput);
389
- if (!fs.existsSync(filePath)) {
390
- throw new Error(`File or directory not found: ${filePath}`);
391
- }
392
-
393
- const stats = fs.statSync(filePath);
394
- if (stats.isDirectory()) {
395
- archive.directory(filePath, false);
396
- } else {
397
- archive.file(filePath, { name: path.basename(filePath) });
398
- }
399
- } else {
400
- throw new Error(`Invalid file format: ${JSON.stringify(file)}`);
401
- }
402
- });
403
-
404
- archive.finalize();
405
- });
406
- }
407
-
408
- /**
409
- * Uploads a buffer to a specified Google Cloud Storage bucket and blob name.
410
- *
411
- * @async
412
- * @param {import('@google-cloud/storage').Bucket} bucket - The Google Cloud Storage bucket object.
413
- * @param {Buffer} buffer - The buffer containing the data to upload.
414
- * @param {string} destinationBlobName - The name for the blob in the bucket.
415
- * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
416
- * @returns {Promise<import('@google-cloud/storage').File>} A promise that resolves with the GCS File object representing the uploaded blob.
417
- * @throws {Error} If the upload fails.
418
- */
419
- async function uploadToStorageBucket(
420
- bucket,
421
- buffer,
422
- destinationBlobName,
423
- progressCallback
424
- ) {
425
- try {
426
- await logAndProgress(
427
- `Uploading buffer to gs://${bucket.name}/${destinationBlobName}...`,
428
- progressCallback
429
- );
430
- await callWithRetry(
431
- () => bucket.file(destinationBlobName).save(buffer),
432
- `storage.bucket.file.save ${destinationBlobName}`
433
- );
434
- await logAndProgress(
435
- `File ${destinationBlobName} uploaded successfully to gs://${bucket.name}/${destinationBlobName}.`,
436
- progressCallback
437
- );
438
- return bucket.file(destinationBlobName);
439
- } catch (error) {
440
- const errorMessage = `Error uploading buffer: ${error.message}`;
441
- console.error(`Error uploading buffer:`, error);
442
- await logAndProgress(errorMessage, progressCallback, 'error');
443
- throw error;
444
- }
445
- }
446
-
447
- /**
448
- * Ensures that an Artifact Registry repository exists.
449
- * If the repository does not exist, it attempts to create it with the specified format.
450
- *
451
- * @async
452
- * @param {string} projectId - The Google Cloud project ID.
453
- * @param {string} location - The Google Cloud region for the repository.
454
- * @param {string} repositoryId - The ID for the Artifact Registry repository.
455
- * @param {string} [format='DOCKER'] - The format of the repository (e.g., 'DOCKER', 'NPM'). Defaults to 'DOCKER'.
456
- * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
457
- * @returns {Promise<object>} A promise that resolves with the Artifact Registry repository object.
458
- * @throws {Error} If there's an error checking or creating the repository.
459
- */
460
- async function ensureArtifactRegistryRepoExists(
461
- projectId,
462
- location,
463
- repositoryId,
464
- format = 'DOCKER',
465
- progressCallback
466
- ) {
467
- const parent = `projects/${projectId}/locations/${location}`;
468
- const repoPath = artifactRegistryClient.repositoryPath(
469
- projectId,
470
- location,
471
- repositoryId
472
- );
473
-
474
- try {
475
- const [repository] = await callWithRetry(
476
- () => artifactRegistryClient.getRepository({ name: repoPath }),
477
- `artifactRegistry.getRepository ${repositoryId}`
478
- );
479
- await logAndProgress(
480
- `Repository ${repositoryId} already exists in ${location}.`,
481
- progressCallback
482
- );
483
- return repository;
484
- } catch (error) {
485
- if (error.code === 5) {
486
- await logAndProgress(
487
- `Repository ${repositoryId} does not exist in ${location}. Creating...`,
488
- progressCallback
489
- );
490
- const repositoryToCreate = {
491
- format: format,
492
- };
493
-
494
- const [operation] = await callWithRetry(
495
- () =>
496
- artifactRegistryClient.createRepository({
497
- parent: parent,
498
- repository: repositoryToCreate,
499
- repositoryId: repositoryId,
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;
513
- } else {
514
- const errorMessage = `Error checking/creating repository ${repositoryId}: ${error.message}`;
515
- console.error(
516
- `Error checking/creating repository ${repositoryId}:`,
517
- error
518
- );
519
- await logAndProgress(errorMessage, progressCallback, 'error');
520
- throw error;
521
- }
522
- }
523
- }
524
-
525
- /**
526
- * Triggers a Google Cloud Build job to build a container image from source code in a GCS bucket.
527
- * It uses either a Dockerfile found in the source or Google Cloud Buildpacks if no Dockerfile is present.
528
- * Waits for the build to complete and returns the build result.
529
- *
530
- * @async
531
- * @param {string} projectId - The Google Cloud project ID.
532
- * @param {string} location - The Google Cloud region for the build.
533
- * @param {string} sourceBucketName - The GCS bucket name where the source code (zip) is stored.
534
- * @param {string} sourceBlobName - The GCS blob name (the zip file) for the source code.
535
- * @param {string} targetRepoName - The name of the target Artifact Registry repository (used for context, not directly in build steps).
536
- * @param {string} targetImageUrl - The full Artifact Registry URL for the image to be built (e.g., `location-docker.pkg.dev/project/repo/image:tag`).
537
- * @param {boolean} hasDockerfile - Indicates whether a Dockerfile is present in the source to guide the build process.
538
- * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
539
- * @returns {Promise<object>} A promise that resolves with the completed Cloud Build object.
540
- * @throws {Error} If the Cloud Build job fails, times out, or encounters an error during initiation or execution.
541
- */
542
- async function triggerCloudBuild(
543
- projectId,
544
- location,
545
- sourceBucketName,
546
- sourceBlobName,
547
- targetRepoName,
548
- targetImageUrl,
549
- hasDockerfile,
550
- progressCallback
551
- ) {
552
- let buildSteps;
553
-
554
- if (hasDockerfile) {
555
- buildSteps = [
556
- {
557
- name: 'gcr.io/cloud-builders/docker',
558
- args: ['build', '-t', targetImageUrl, '.'],
559
- dir: '/workspace',
560
- },
561
- ];
562
- } else {
563
- buildSteps = [
564
- {
565
- name: 'gcr.io/k8s-skaffold/pack',
566
- entrypoint: 'pack',
567
- args: [
568
- 'build',
569
- targetImageUrl,
570
- '--builder',
571
- 'gcr.io/buildpacks/builder:latest',
572
- ],
573
- dir: '/workspace',
574
- },
575
- ];
576
- }
577
-
578
- const build = {
579
- source: {
580
- storageSource: {
581
- bucket: sourceBucketName,
582
- object: sourceBlobName,
583
- },
584
- },
585
- steps: buildSteps,
586
- images: [targetImageUrl],
587
- };
588
-
589
- await logAndProgress(
590
- `Initiating Cloud Build for gs://${sourceBucketName}/${sourceBlobName} in ${location}...`,
591
- progressCallback
592
- );
593
- const [operation] = await callWithRetry(
594
- () => cloudBuildClient.createBuild({ projectId: projectId, build: build }),
595
- 'cloudBuild.createBuild'
596
- );
597
-
598
- await logAndProgress(`Cloud Build job started...`, progressCallback);
599
- const buildId = operation.metadata.build.id;
600
- let completedBuild;
601
- while (true) {
602
- const [getBuildOperation] = await callWithRetry(
603
- () => cloudBuildClient.getBuild({ projectId: projectId, id: buildId }),
604
- `cloudBuild.getBuild ${buildId}`
605
- );
606
- if (
607
- ['SUCCESS', 'FAILURE', 'INTERNAL_ERROR', 'TIMEOUT', 'CANCELLED'].includes(
608
- getBuildOperation.status
609
- )
610
- ) {
611
- completedBuild = getBuildOperation;
612
- break;
613
- }
614
- await logAndProgress(
615
- `Build status: ${getBuildOperation.status}. Waiting...`,
616
- progressCallback,
617
- 'debug'
618
- );
619
- await new Promise((resolve) => setTimeout(resolve, 5000));
620
- }
621
-
622
- if (completedBuild.status === 'SUCCESS') {
623
- await logAndProgress(
624
- `Cloud Build job ${buildId} completed successfully.`,
625
- progressCallback
626
- );
627
- await logAndProgress(
628
- `Image built: ${completedBuild.results.images[0].name}`,
629
- progressCallback
630
- );
631
- return completedBuild;
632
- } else {
633
- const failureMessage = `Cloud Build job ${buildId} failed with status: ${completedBuild.status}`;
634
- await logAndProgress(failureMessage, progressCallback, 'error');
635
- const logsMessage = `Build logs: ${completedBuild.logUrl}`;
636
- await logAndProgress(logsMessage, progressCallback); // Log URL is info, failure is error
637
-
638
- let buildLogsSnippet = `\n\nRefer to Log URL for full details: ${completedBuild.logUrl}`; // Default snippet
639
- try {
640
- const { Logging } = await import('@google-cloud/logging');
641
- const loggingClient = new Logging({ projectId });
642
-
643
- const logFilter = `resource.type="build" AND resource.labels.build_id="${buildId}"`;
644
- await logAndProgress(
645
- `Attempting to fetch last ${BUILD_LOGS_LINES_TO_FETCH} log lines for build ${buildId}...`,
646
- progressCallback,
647
- 'debug'
648
- );
649
-
650
- // Wait for a short period to allow logs to propagate
651
- await new Promise((resolve) =>
652
- setTimeout(resolve, DELAY_WAIT_FOR_BUILD_LOGS)
653
- );
654
-
655
- // Fetch the most recent N log entries
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
- );
665
-
666
- if (entries && entries.length > 0) {
667
- // Entries are newest first, reverse for chronological order of the snippet
668
- const logLines = entries.reverse().map((entry) => entry.data || '');
669
- if (logLines.length > 0) {
670
- buildLogsSnippet = `\n\nLast ${logLines.length} log lines from build ${buildId}:\n${logLines.join('\n')}`;
671
- await logAndProgress(
672
- `Successfully fetched snippet of build logs for ${buildId}.`,
673
- progressCallback,
674
- 'info'
675
- );
676
- }
677
- } else {
678
- await logAndProgress(
679
- `No specific log entries retrieved for build ${buildId}. ${buildLogsSnippet}`,
680
- progressCallback,
681
- 'warn'
682
- );
683
- }
684
- } catch (logError) {
685
- console.error(`Error fetching build logs for ${buildId}:`, logError);
686
- await logAndProgress(
687
- `Failed to fetch build logs snippet: ${logError.message}. ${buildLogsSnippet}`,
688
- progressCallback,
689
- 'warn'
690
- );
691
- // buildLogsSnippet already contains the Log URL as a fallback
692
- }
693
- throw new Error(`Build ${buildId} failed.${buildLogsSnippet}`);
694
- }
695
- }
696
-
697
- /**
698
- * Deploys a service to Google Cloud Run.
699
- * @param {object} config - The deployment configuration.
700
- * @param {string} config.projectId - The Google Cloud project ID.
701
- * @param {string} [config.serviceName='app'] - The name of the Cloud Run service. Defaults to 'app'.
702
- * @param {string} [config.region='europe-west1'] - The Google Cloud region for deployment. Defaults to 'europe-west1'.
703
- * @param {Array<string|{filename: string, content: Buffer|string}>} config.files - An array of file paths or file objects (with `filename` and `content`) to deploy.
704
- * @param {function(object): void} [config.progressCallback] - Optional callback for progress updates. Receives an object with `level` and `data` properties.
705
- * @returns {Promise<object>} A promise that resolves with the deployed Cloud Run service object.
706
- * @throws {Error} If deployment fails or required configuration is missing.
707
- */
708
- export async function deploy({
709
- projectId,
710
- serviceName,
711
- region,
712
- files,
713
- progressCallback,
714
- skipIamCheck,
715
- }) {
716
- if (!projectId) {
717
- const errorMsg =
718
- 'Error: projectId is required in the configuration object.';
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);
728
- }
729
-
730
- if (!files || !Array.isArray(files) || files.length === 0) {
731
- const errorMsg =
732
- 'Error: files array is required in the configuration object.';
733
- await logAndProgress(errorMsg, progressCallback, 'error');
734
- if (typeof process !== 'undefined' && process.exit) {
735
- process.exit(1);
736
- } else {
737
- throw new Error(errorMsg);
738
- }
739
- }
740
-
741
- const path = await import('path');
742
- const fs = await import('fs');
743
- const { Storage } = await import('@google-cloud/storage');
744
- const { CloudBuildClient } = await import('@google-cloud/cloudbuild');
745
- const { ArtifactRegistryClient } = await import(
746
- '@google-cloud/artifact-registry'
747
- );
748
- const { v2: CloudRunV2Module } = await import('@google-cloud/run');
749
- const { ServicesClient } = CloudRunV2Module;
750
-
751
- try {
752
- await ensureApisEnabled(
753
- projectId,
754
- REQUIRED_APIS_FOR_SOURCE_DEPLOY,
755
- progressCallback
756
- );
757
-
758
- storage = new Storage({ projectId });
759
- cloudBuildClient = new CloudBuildClient({ projectId });
760
- artifactRegistryClient = new ArtifactRegistryClient({ projectId });
761
- runClient = new ServicesClient({ projectId });
762
-
763
- const bucketName = `${projectId}-source-bucket`;
764
- const imageUrl = `${region}-docker.pkg.dev/${projectId}/${REPO_NAME}/${serviceName}:${IMAGE_TAG}`;
765
-
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);
770
-
771
- let hasDockerfile = false;
772
- if (
773
- files.length === 1 &&
774
- typeof files[0] === 'string' &&
775
- fs.statSync(files[0]).isDirectory()
776
- ) {
777
- // Handle folder deployment: check for Dockerfile inside the folder
778
- const dockerfilePath = path.join(files[0], 'Dockerfile');
779
- const dockerfilePathLowerCase = path.join(files[0], 'dockerfile');
780
- if (
781
- fs.existsSync(dockerfilePath) ||
782
- fs.existsSync(dockerfilePathLowerCase)
783
- ) {
784
- hasDockerfile = true;
785
- }
786
- } else {
787
- // Handle file list deployment or file content deployment
788
- for (const file of files) {
789
- if (typeof file === 'string') {
790
- if (path.basename(file).toLowerCase() === 'dockerfile') {
791
- hasDockerfile = true;
792
- break;
793
- }
794
- } else if (typeof file === 'object' && file.filename) {
795
- if (path.basename(file.filename).toLowerCase() === 'dockerfile') {
796
- hasDockerfile = true;
797
- break;
798
- }
799
- }
800
- }
801
- }
802
- await logAndProgress(`Dockerfile: ${hasDockerfile}`, progressCallback);
803
-
804
- const bucket = await ensureStorageBucketExists(
805
- bucketName,
806
- region,
807
- progressCallback
808
- );
809
-
810
- const zipBuffer = await zipFiles(files, progressCallback);
811
- await uploadToStorageBucket(
812
- bucket,
813
- zipBuffer,
814
- ZIP_FILE_NAME,
815
- progressCallback
816
- );
817
- await logAndProgress('Source code uploaded successfully', progressCallback);
818
-
819
- await ensureArtifactRegistryRepoExists(
820
- projectId,
821
- region,
822
- REPO_NAME,
823
- 'DOCKER',
824
- progressCallback
825
- );
826
-
827
- const buildResult = await triggerCloudBuild(
828
- projectId,
829
- region,
830
- bucketName,
831
- ZIP_FILE_NAME,
832
- REPO_NAME,
833
- imageUrl,
834
- hasDockerfile,
835
- progressCallback
836
- );
837
-
838
- const builtImageUrl = buildResult.results.images[0].name;
839
-
840
- const service = await deployToCloudRun(
841
- projectId,
842
- region,
843
- serviceName,
844
- builtImageUrl,
845
- progressCallback,
846
- skipIamCheck
847
- );
848
-
849
- await logAndProgress(`Deployment Completed Successfully`, progressCallback);
850
- return service;
851
- } catch (error) {
852
- const deployFailedMessage = `Deployment Failed: ${error.message}`;
853
- console.error(`Deployment Failed`, error);
854
- await logAndProgress(deployFailedMessage, progressCallback, 'error');
855
- throw error;
856
- }
857
- }
858
-
859
- /**
860
- * Deploys a container image to Google Cloud Run.
861
- * @param {object} config - The deployment configuration.
862
- * @param {string} config.projectId - The Google Cloud project ID.
863
- * @param {string} [config.serviceName='app'] - The name of the Cloud Run service. Defaults to 'app'.
864
- * @param {string} [config.region='europe-west1'] - The Google Cloud region for deployment. Defaults to 'europe-west1'.
865
- * @param {string} config.imageUrl - The URL of the container image to deploy.
866
- * @param {function(object): void} [config.progressCallback] - Optional callback for progress updates. Receives an object with `level` and `data` properties.
867
- * @param {boolean} [config.skipIamCheck=false] - Whether to skip the IAM check.
868
- * @returns {Promise<object>} A promise that resolves with the deployed Cloud Run service object.
869
- * @throws {Error} If deployment fails or required configuration is missing.
870
- */
871
- export async function deployImage({
872
- projectId,
873
- serviceName,
874
- region,
875
- imageUrl,
876
- progressCallback,
877
- skipIamCheck,
878
- }) {
879
- if (!projectId) {
880
- const errorMsg =
881
- 'Error: projectId is required in the configuration object.';
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);
891
- }
892
-
893
- if (!imageUrl) {
894
- const errorMsg = 'Error: imageUrl is required in the configuration object.';
895
- await logAndProgress(errorMsg, progressCallback, 'error');
896
- if (typeof process !== 'undefined' && process.exit) {
897
- process.exit(1);
898
- } else {
899
- throw new Error(errorMsg);
900
- }
901
- }
902
-
903
- const { v2: CloudRunV2Module } = await import('@google-cloud/run');
904
- const { ServicesClient } = CloudRunV2Module;
905
-
906
- try {
907
- await ensureApisEnabled(
908
- projectId,
909
- REQUIRED_APIS_FOR_IMAGE_DEPLOY,
910
- progressCallback
911
- );
912
-
913
- runClient = new ServicesClient({ projectId });
914
-
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);
919
-
920
- const service = await deployToCloudRun(
921
- projectId,
922
- region,
923
- serviceName,
924
- imageUrl,
925
- progressCallback,
926
- skipIamCheck
927
- );
928
-
929
- await logAndProgress(`Deployment Completed Successfully`, progressCallback);
930
- return service;
931
- } catch (error) {
932
- const deployFailedMessage = `Deployment Failed: ${error.message}`;
933
- console.error(`Deployment Failed`, error);
934
- await logAndProgress(deployFailedMessage, progressCallback, 'error');
935
- throw error;
936
- }
937
- }