@google-cloud/cloud-run-mcp 1.3.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 (44) hide show
  1. package/.c8rc.json +9 -0
  2. package/.github/images/deploy_from_apps.gif +0 -0
  3. package/.github/images/deploy_from_ide.gif +0 -0
  4. package/.github/images/deploycli.gif +0 -0
  5. package/.github/workflows/lint-checker.yml +17 -0
  6. package/.github/workflows/local-tests.yml +1 -1
  7. package/.kokoro/kokoro_build.sh +22 -0
  8. package/.kokoro/presubmit.cfg +21 -0
  9. package/.kokoro/run_tests.sh +31 -0
  10. package/CONTRIBUTING.md +29 -8
  11. package/README.md +65 -19
  12. package/REVIEWING.md +20 -0
  13. package/gemini-extension/GEMINI.md +3 -3
  14. package/lib/cloud-api/build.js +200 -0
  15. package/lib/{gcp-api-helpers.js → cloud-api/helpers.js} +12 -8
  16. package/lib/{gcp-projects.js → cloud-api/projects.js} +58 -28
  17. package/lib/cloud-api/registry.js +98 -0
  18. package/lib/{cloud-run-services.js → cloud-api/run.js} +103 -21
  19. package/lib/cloud-api/storage.js +123 -0
  20. package/lib/deployment/deployer.js +474 -0
  21. package/lib/util/archive.js +95 -0
  22. package/lib/util/helpers.js +42 -0
  23. package/mcp-server.js +29 -11
  24. package/package.json +9 -6
  25. package/test/local/cloud-api/build.test.js +436 -0
  26. package/test/local/cloud-api/projects.test.js +18 -0
  27. package/test/local/mcp-server-streamable-http.test.js +57 -0
  28. package/test/local/mcp-server.test.js +0 -1
  29. package/test/local/notifications.test.js +48 -0
  30. package/test/local/tools.test.js +143 -124
  31. package/test/need-gcp/cloud-run-services.test.js +57 -77
  32. package/test/need-gcp/deployer.test.js +71 -0
  33. package/test/need-gcp/gcp-auth-check.test.js +1 -1
  34. package/test/need-gcp/gcp-projects.test.js +6 -60
  35. package/test/need-gcp/test-helpers.js +175 -0
  36. package/test/need-gcp/workflows/deployment-workflows.test.js +100 -0
  37. package/tools/register-tools.js +612 -0
  38. package/tools/tools.js +48 -0
  39. package/lib/cloud-run-deploy.js +0 -917
  40. package/test/need-gcp/cloud-run-deploy.test.js +0 -158
  41. package/tools.js +0 -969
  42. /package/lib/{gcp-auth-check.js → cloud-api/auth.js} +0 -0
  43. /package/lib/{gcp-billing-accounts.js → cloud-api/billing.js} +0 -0
  44. /package/lib/{gcp-metadata.js → cloud-api/metadata.js} +0 -0
@@ -1,917 +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
- 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
- logAndProgress(
93
- `Cloud Run service ${serviceId} already exists.`,
94
- progressCallback
95
- );
96
- return true;
97
- } catch (error) {
98
- if (error.code === 5) {
99
- 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
- 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
- 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
- logAndProgress(
193
- `Dry run successful for ${serviceId} with current configuration.`,
194
- progressCallback,
195
- 'debug'
196
- );
197
- } catch (dryRunError) {
198
- 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
- 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
- logAndProgress(errorMessage, progressCallback, 'error');
222
- throw new Error(errorMessage);
223
- }
224
- }
225
-
226
- let operation;
227
- if (exists) {
228
- 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
- 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
- logAndProgress(`Deploying ${serviceId} to Cloud Run...`, progressCallback);
251
- const [response] = await operation.promise();
252
-
253
- logAndProgress(
254
- `Service deployed/updated successfully: ${response.uri}`,
255
- progressCallback
256
- );
257
- return response;
258
- } catch (error) {
259
- const errorMessage = `Error deploying/updating service ${serviceId}: ${error.message}`;
260
- console.error(`Error deploying/updating service ${serviceId}:`, error);
261
- logAndProgress(errorMessage, progressCallback, 'error');
262
- throw error;
263
- }
264
- }
265
-
266
- /**
267
- * Ensures that a Google Cloud Storage bucket exists.
268
- * If the bucket does not exist, it attempts to create it in the specified location.
269
- *
270
- * @async
271
- * @param {string} bucketName - The name of the storage bucket.
272
- * @param {string} [location='us'] - The location to create the bucket in if it doesn't exist. Defaults to 'us'.
273
- * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
274
- * @returns {Promise<import('@google-cloud/storage').Bucket>} A promise that resolves with the GCS Bucket object.
275
- * @throws {Error} If there's an error checking or creating the bucket.
276
- */
277
- async function ensureStorageBucketExists(
278
- bucketName,
279
- location = 'us',
280
- progressCallback
281
- ) {
282
- const bucket = storage.bucket(bucketName);
283
- try {
284
- const [exists] = await callWithRetry(
285
- () => bucket.exists(),
286
- `storage.bucket.exists ${bucketName}`
287
- );
288
- if (exists) {
289
- logAndProgress(`Bucket ${bucketName} already exists.`, progressCallback);
290
- return bucket;
291
- } else {
292
- logAndProgress(
293
- `Bucket ${bucketName} does not exist. Creating in location ${location}...`,
294
- progressCallback
295
- );
296
- try {
297
- const [createdBucket] = await callWithRetry(
298
- () => storage.createBucket(bucketName, { location: location }),
299
- `storage.createBucket ${bucketName}`
300
- );
301
- logAndProgress(
302
- `Storage bucket ${createdBucket.name} created successfully in ${location}.`,
303
- progressCallback
304
- );
305
- return createdBucket;
306
- } catch (createError) {
307
- const errorMessage = `Failed to create storage bucket ${bucketName}. Error details: ${createError.message}`;
308
- console.error(
309
- `Failed to create storage bucket ${bucketName}. Error details:`,
310
- createError
311
- );
312
- logAndProgress(errorMessage, progressCallback, 'error');
313
- throw createError;
314
- }
315
- }
316
- } catch (error) {
317
- const errorMessage = `Error checking/creating bucket ${bucketName}: ${error.message}`;
318
- console.error(`Error checking/creating bucket ${bucketName}:`, error);
319
- logAndProgress(errorMessage, progressCallback, 'error');
320
- throw error;
321
- }
322
- }
323
-
324
- /**
325
- * Creates a zip archive in memory from a list of file paths and/or file objects.
326
- * File objects should have `filename` (string) and `content` (Buffer or string) properties.
327
- *
328
- * @param {Array<string|{filename: string, content: Buffer|string}>} files - An array of items to zip.
329
- * Each item can be a string representing a file/directory path, or an object
330
- * with `filename` and `content` properties for in-memory files.
331
- * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
332
- * @returns {Promise<Buffer>} A promise that resolves with a Buffer containing the zip data.
333
- * @throws {Error} If an input file path is not found, an input item has an invalid format, or an archiver error occurs.
334
- */
335
- async function zipFiles(files, progressCallback) {
336
- const path = await import('path');
337
- const fs = await import('fs');
338
- const archiver = (await import('archiver')).default;
339
-
340
- return new Promise((resolve, reject) => {
341
- logAndProgress('Creating in-memory zip archive...', progressCallback);
342
- const chunks = [];
343
- const archive = archiver('zip', {
344
- zlib: { level: 9 },
345
- });
346
-
347
- archive.on('data', (chunk) => chunks.push(chunk));
348
- archive.on('end', () => {
349
- logAndProgress(
350
- `Files zipped successfully. Total size: ${archive.pointer()} bytes`,
351
- progressCallback
352
- );
353
- resolve(Buffer.concat(chunks));
354
- });
355
-
356
- archive.on('warning', (err) => {
357
- const warningMessage = `Archiver warning: ${err}`;
358
- logAndProgress(warningMessage, progressCallback, 'warn');
359
- if (err.code !== 'ENOENT') {
360
- // ENOENT is often just a warning, others might be more critical for zip
361
- reject(err);
362
- }
363
- });
364
-
365
- archive.on('error', (err) => {
366
- const errorMessage = `Archiver error: ${err.message}`;
367
- console.error(errorMessage, err);
368
- logAndProgress(errorMessage, progressCallback, 'error');
369
- reject(err);
370
- });
371
-
372
- files.forEach((file) => {
373
- if (typeof file === 'object' && 'filename' in file && 'content' in file) {
374
- archive.append(file.content, { name: file.filename });
375
- } else if (typeof file === 'string') {
376
- let pathInput = file;
377
-
378
- // 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'
379
- if (pathInput.startsWith('/c')) {
380
- pathInput = `/mnt${pathInput}`;
381
- }
382
- const filePath = path.resolve(pathInput);
383
- if (!fs.existsSync(filePath)) {
384
- throw new Error(`File or directory not found: ${filePath}`);
385
- }
386
-
387
- const stats = fs.statSync(filePath);
388
- if (stats.isDirectory()) {
389
- archive.directory(filePath, false);
390
- } else {
391
- archive.file(filePath, { name: path.basename(filePath) });
392
- }
393
- } else {
394
- throw new Error(`Invalid file format: ${JSON.stringify(file)}`);
395
- }
396
- });
397
-
398
- archive.finalize();
399
- });
400
- }
401
-
402
- /**
403
- * Uploads a buffer to a specified Google Cloud Storage bucket and blob name.
404
- *
405
- * @async
406
- * @param {import('@google-cloud/storage').Bucket} bucket - The Google Cloud Storage bucket object.
407
- * @param {Buffer} buffer - The buffer containing the data to upload.
408
- * @param {string} destinationBlobName - The name for the blob in the bucket.
409
- * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
410
- * @returns {Promise<import('@google-cloud/storage').File>} A promise that resolves with the GCS File object representing the uploaded blob.
411
- * @throws {Error} If the upload fails.
412
- */
413
- async function uploadToStorageBucket(
414
- bucket,
415
- buffer,
416
- destinationBlobName,
417
- progressCallback
418
- ) {
419
- try {
420
- logAndProgress(
421
- `Uploading buffer to gs://${bucket.name}/${destinationBlobName}...`,
422
- progressCallback
423
- );
424
- await callWithRetry(
425
- () => bucket.file(destinationBlobName).save(buffer),
426
- `storage.bucket.file.save ${destinationBlobName}`
427
- );
428
- logAndProgress(
429
- `File ${destinationBlobName} uploaded successfully to gs://${bucket.name}/${destinationBlobName}.`,
430
- progressCallback
431
- );
432
- return bucket.file(destinationBlobName);
433
- } catch (error) {
434
- const errorMessage = `Error uploading buffer: ${error.message}`;
435
- console.error(`Error uploading buffer:`, error);
436
- logAndProgress(errorMessage, progressCallback, 'error');
437
- throw error;
438
- }
439
- }
440
-
441
- /**
442
- * Ensures that an Artifact Registry repository exists.
443
- * If the repository does not exist, it attempts to create it with the specified format.
444
- *
445
- * @async
446
- * @param {string} projectId - The Google Cloud project ID.
447
- * @param {string} location - The Google Cloud region for the repository.
448
- * @param {string} repositoryId - The ID for the Artifact Registry repository.
449
- * @param {string} [format='DOCKER'] - The format of the repository (e.g., 'DOCKER', 'NPM'). Defaults to 'DOCKER'.
450
- * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
451
- * @returns {Promise<object>} A promise that resolves with the Artifact Registry repository object.
452
- * @throws {Error} If there's an error checking or creating the repository.
453
- */
454
- async function ensureArtifactRegistryRepoExists(
455
- projectId,
456
- location,
457
- repositoryId,
458
- format = 'DOCKER',
459
- progressCallback
460
- ) {
461
- const parent = `projects/${projectId}/locations/${location}`;
462
- const repoPath = artifactRegistryClient.repositoryPath(
463
- projectId,
464
- location,
465
- repositoryId
466
- );
467
-
468
- try {
469
- const [repository] = await callWithRetry(
470
- () => artifactRegistryClient.getRepository({ name: repoPath }),
471
- `artifactRegistry.getRepository ${repositoryId}`
472
- );
473
- logAndProgress(
474
- `Repository ${repositoryId} already exists in ${location}.`,
475
- progressCallback
476
- );
477
- return repository;
478
- } catch (error) {
479
- if (error.code === 5) {
480
- logAndProgress(
481
- `Repository ${repositoryId} does not exist in ${location}. Creating...`,
482
- progressCallback
483
- );
484
- const repositoryToCreate = {
485
- format: format,
486
- };
487
-
488
- const [operation] = await callWithRetry(
489
- () =>
490
- artifactRegistryClient.createRepository({
491
- parent: parent,
492
- repository: repositoryToCreate,
493
- repositoryId: repositoryId,
494
- }),
495
- `artifactRegistry.createRepository ${repositoryId}`
496
- );
497
- logAndProgress(
498
- `Creating Artifact Registry repository ${repositoryId}...`,
499
- progressCallback
500
- );
501
- const [result] = await operation.promise();
502
- logAndProgress(
503
- `Artifact Registry repository ${result.name} created successfully.`,
504
- progressCallback
505
- );
506
- return result;
507
- } else {
508
- const errorMessage = `Error checking/creating repository ${repositoryId}: ${error.message}`;
509
- console.error(
510
- `Error checking/creating repository ${repositoryId}:`,
511
- error
512
- );
513
- logAndProgress(errorMessage, progressCallback, 'error');
514
- throw error;
515
- }
516
- }
517
- }
518
-
519
- /**
520
- * Triggers a Google Cloud Build job to build a container image from source code in a GCS bucket.
521
- * It uses either a Dockerfile found in the source or Google Cloud Buildpacks if no Dockerfile is present.
522
- * Waits for the build to complete and returns the build result.
523
- *
524
- * @async
525
- * @param {string} projectId - The Google Cloud project ID.
526
- * @param {string} location - The Google Cloud region for the build.
527
- * @param {string} sourceBucketName - The GCS bucket name where the source code (zip) is stored.
528
- * @param {string} sourceBlobName - The GCS blob name (the zip file) for the source code.
529
- * @param {string} targetRepoName - The name of the target Artifact Registry repository (used for context, not directly in build steps).
530
- * @param {string} targetImageUrl - The full Artifact Registry URL for the image to be built (e.g., `location-docker.pkg.dev/project/repo/image:tag`).
531
- * @param {boolean} hasDockerfile - Indicates whether a Dockerfile is present in the source to guide the build process.
532
- * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
533
- * @returns {Promise<object>} A promise that resolves with the completed Cloud Build object.
534
- * @throws {Error} If the Cloud Build job fails, times out, or encounters an error during initiation or execution.
535
- */
536
- async function triggerCloudBuild(
537
- projectId,
538
- location,
539
- sourceBucketName,
540
- sourceBlobName,
541
- targetRepoName,
542
- targetImageUrl,
543
- hasDockerfile,
544
- progressCallback
545
- ) {
546
- let buildSteps;
547
-
548
- if (hasDockerfile) {
549
- buildSteps = [
550
- {
551
- name: 'gcr.io/cloud-builders/docker',
552
- args: ['build', '-t', targetImageUrl, '.'],
553
- dir: '/workspace',
554
- },
555
- ];
556
- } else {
557
- buildSteps = [
558
- {
559
- name: 'gcr.io/k8s-skaffold/pack',
560
- entrypoint: 'pack',
561
- args: [
562
- 'build',
563
- targetImageUrl,
564
- '--builder',
565
- 'gcr.io/buildpacks/builder:latest',
566
- ],
567
- dir: '/workspace',
568
- },
569
- ];
570
- }
571
-
572
- const build = {
573
- source: {
574
- storageSource: {
575
- bucket: sourceBucketName,
576
- object: sourceBlobName,
577
- },
578
- },
579
- steps: buildSteps,
580
- images: [targetImageUrl],
581
- };
582
-
583
- logAndProgress(
584
- `Initiating Cloud Build for gs://${sourceBucketName}/${sourceBlobName} in ${location}...`,
585
- progressCallback
586
- );
587
- const [operation] = await callWithRetry(
588
- () => cloudBuildClient.createBuild({ projectId: projectId, build: build }),
589
- 'cloudBuild.createBuild'
590
- );
591
-
592
- logAndProgress(`Cloud Build job started...`, progressCallback);
593
- const buildId = operation.metadata.build.id;
594
- let completedBuild;
595
- while (true) {
596
- const [getBuildOperation] = await callWithRetry(
597
- () => cloudBuildClient.getBuild({ projectId: projectId, id: buildId }),
598
- `cloudBuild.getBuild ${buildId}`
599
- );
600
- if (
601
- ['SUCCESS', 'FAILURE', 'INTERNAL_ERROR', 'TIMEOUT', 'CANCELLED'].includes(
602
- getBuildOperation.status
603
- )
604
- ) {
605
- completedBuild = getBuildOperation;
606
- break;
607
- }
608
- logAndProgress(
609
- `Build status: ${getBuildOperation.status}. Waiting...`,
610
- progressCallback,
611
- 'debug'
612
- );
613
- await new Promise((resolve) => setTimeout(resolve, 5000));
614
- }
615
-
616
- if (completedBuild.status === 'SUCCESS') {
617
- logAndProgress(
618
- `Cloud Build job ${buildId} completed successfully.`,
619
- progressCallback
620
- );
621
- logAndProgress(
622
- `Image built: ${completedBuild.results.images[0].name}`,
623
- progressCallback
624
- );
625
- return completedBuild;
626
- } else {
627
- const failureMessage = `Cloud Build job ${buildId} failed with status: ${completedBuild.status}`;
628
- logAndProgress(failureMessage, progressCallback, 'error');
629
- const logsMessage = `Build logs: ${completedBuild.logUrl}`;
630
- logAndProgress(logsMessage, progressCallback); // Log URL is info, failure is error
631
-
632
- let buildLogsSnippet = `\n\nRefer to Log URL for full details: ${completedBuild.logUrl}`; // Default snippet
633
- try {
634
- const { Logging } = await import('@google-cloud/logging');
635
- const loggingClient = new Logging({ projectId });
636
-
637
- const logFilter = `resource.type="build" AND resource.labels.build_id="${buildId}"`;
638
- logAndProgress(
639
- `Attempting to fetch last ${BUILD_LOGS_LINES_TO_FETCH} log lines for build ${buildId}...`,
640
- progressCallback,
641
- 'debug'
642
- );
643
-
644
- // Wait for a short period to allow logs to propagate
645
- await new Promise((resolve) =>
646
- setTimeout(resolve, DELAY_WAIT_FOR_BUILD_LOGS)
647
- );
648
-
649
- // Fetch the most recent N log entries
650
- const [entries] = await callWithRetry(
651
- () =>
652
- loggingClient.getEntries({
653
- filter: logFilter,
654
- orderBy: 'timestamp desc', // Get latest logs first
655
- pageSize: BUILD_LOGS_LINES_TO_FETCH,
656
- }),
657
- `logging.getEntries for build ${buildId}`
658
- );
659
-
660
- if (entries && entries.length > 0) {
661
- // Entries are newest first, reverse for chronological order of the snippet
662
- const logLines = entries.reverse().map((entry) => entry.data || '');
663
- if (logLines.length > 0) {
664
- buildLogsSnippet = `\n\nLast ${logLines.length} log lines from build ${buildId}:\n${logLines.join('\n')}`;
665
- logAndProgress(
666
- `Successfully fetched snippet of build logs for ${buildId}.`,
667
- progressCallback,
668
- 'info'
669
- );
670
- }
671
- } else {
672
- logAndProgress(
673
- `No specific log entries retrieved for build ${buildId}. ${buildLogsSnippet}`,
674
- progressCallback,
675
- 'warn'
676
- );
677
- }
678
- } catch (logError) {
679
- console.error(`Error fetching build logs for ${buildId}:`, logError);
680
- logAndProgress(
681
- `Failed to fetch build logs snippet: ${logError.message}. ${buildLogsSnippet}`,
682
- progressCallback,
683
- 'warn'
684
- );
685
- // buildLogsSnippet already contains the Log URL as a fallback
686
- }
687
- throw new Error(`Build ${buildId} failed.${buildLogsSnippet}`);
688
- }
689
- }
690
-
691
- /**
692
- * Deploys a service to Google Cloud Run.
693
- * @param {object} config - The deployment configuration.
694
- * @param {string} config.projectId - The Google Cloud project ID.
695
- * @param {string} [config.serviceName='app'] - The name of the Cloud Run service. Defaults to 'app'.
696
- * @param {string} [config.region='europe-west1'] - The Google Cloud region for deployment. Defaults to 'europe-west1'.
697
- * @param {Array<string|{filename: string, content: Buffer|string}>} config.files - An array of file paths or file objects (with `filename` and `content`) to deploy.
698
- * @param {function(object): void} [config.progressCallback] - Optional callback for progress updates. Receives an object with `level` and `data` properties.
699
- * @returns {Promise<object>} A promise that resolves with the deployed Cloud Run service object.
700
- * @throws {Error} If deployment fails or required configuration is missing.
701
- */
702
- export async function deploy({
703
- projectId,
704
- serviceName,
705
- region,
706
- files,
707
- progressCallback,
708
- skipIamCheck,
709
- }) {
710
- if (!projectId) {
711
- const errorMsg =
712
- 'Error: projectId is required in the configuration object.';
713
- logAndProgress(errorMsg, progressCallback, 'error');
714
- process.exit(1);
715
- }
716
-
717
- if (!files || !Array.isArray(files) || files.length === 0) {
718
- const errorMsg =
719
- 'Error: files array is required in the configuration object.';
720
- logAndProgress(errorMsg, progressCallback, 'error');
721
- if (typeof process !== 'undefined' && process.exit) {
722
- process.exit(1);
723
- } else {
724
- throw new Error(errorMsg);
725
- }
726
- }
727
-
728
- const path = await import('path');
729
- const fs = await import('fs');
730
- const { Storage } = await import('@google-cloud/storage');
731
- const { CloudBuildClient } = await import('@google-cloud/cloudbuild');
732
- const { ArtifactRegistryClient } = await import(
733
- '@google-cloud/artifact-registry'
734
- );
735
- const { v2: CloudRunV2Module } = await import('@google-cloud/run');
736
- const { ServicesClient } = CloudRunV2Module;
737
-
738
- try {
739
- await ensureApisEnabled(
740
- projectId,
741
- REQUIRED_APIS_FOR_SOURCE_DEPLOY,
742
- progressCallback
743
- );
744
-
745
- storage = new Storage({ projectId });
746
- cloudBuildClient = new CloudBuildClient({ projectId });
747
- artifactRegistryClient = new ArtifactRegistryClient({ projectId });
748
- runClient = new ServicesClient({ projectId });
749
-
750
- const bucketName = `${projectId}-source-bucket`;
751
- const imageUrl = `${region}-docker.pkg.dev/${projectId}/${REPO_NAME}/${serviceName}:${IMAGE_TAG}`;
752
-
753
- logAndProgress(`Project: ${projectId}`, progressCallback);
754
- logAndProgress(`Region: ${region}`, progressCallback);
755
- logAndProgress(`Service Name: ${serviceName}`, progressCallback);
756
- logAndProgress(`Files to deploy: ${files.length}`, progressCallback);
757
-
758
- let hasDockerfile = false;
759
- if (
760
- files.length === 1 &&
761
- typeof files[0] === 'string' &&
762
- fs.statSync(files[0]).isDirectory()
763
- ) {
764
- // Handle folder deployment: check for Dockerfile inside the folder
765
- const dockerfilePath = path.join(files[0], 'Dockerfile');
766
- const dockerfilePathLowerCase = path.join(files[0], 'dockerfile');
767
- if (
768
- fs.existsSync(dockerfilePath) ||
769
- fs.existsSync(dockerfilePathLowerCase)
770
- ) {
771
- hasDockerfile = true;
772
- }
773
- } else {
774
- // Handle file list deployment or file content deployment
775
- for (const file of files) {
776
- if (typeof file === 'string') {
777
- if (path.basename(file).toLowerCase() === 'dockerfile') {
778
- hasDockerfile = true;
779
- break;
780
- }
781
- } else if (typeof file === 'object' && file.filename) {
782
- if (path.basename(file.filename).toLowerCase() === 'dockerfile') {
783
- hasDockerfile = true;
784
- break;
785
- }
786
- }
787
- }
788
- }
789
- logAndProgress(`Dockerfile: ${hasDockerfile}`, progressCallback);
790
-
791
- const bucket = await ensureStorageBucketExists(
792
- bucketName,
793
- region,
794
- progressCallback
795
- );
796
-
797
- const zipBuffer = await zipFiles(files, progressCallback);
798
- await uploadToStorageBucket(
799
- bucket,
800
- zipBuffer,
801
- ZIP_FILE_NAME,
802
- progressCallback
803
- );
804
- logAndProgress('Source code uploaded successfully', progressCallback);
805
-
806
- await ensureArtifactRegistryRepoExists(
807
- projectId,
808
- region,
809
- REPO_NAME,
810
- 'DOCKER',
811
- progressCallback
812
- );
813
-
814
- const buildResult = await triggerCloudBuild(
815
- projectId,
816
- region,
817
- bucketName,
818
- ZIP_FILE_NAME,
819
- REPO_NAME,
820
- imageUrl,
821
- hasDockerfile,
822
- progressCallback
823
- );
824
-
825
- const builtImageUrl = buildResult.results.images[0].name;
826
-
827
- const service = await deployToCloudRun(
828
- projectId,
829
- region,
830
- serviceName,
831
- builtImageUrl,
832
- progressCallback,
833
- skipIamCheck
834
- );
835
-
836
- logAndProgress(`Deployment Completed Successfully`, progressCallback);
837
- return service;
838
- } catch (error) {
839
- const deployFailedMessage = `Deployment Failed: ${error.message}`;
840
- console.error(`Deployment Failed`, error);
841
- logAndProgress(deployFailedMessage, progressCallback, 'error');
842
- throw error;
843
- }
844
- }
845
-
846
- /**
847
- * Deploys a container image to Google Cloud Run.
848
- * @param {object} config - The deployment configuration.
849
- * @param {string} config.projectId - The Google Cloud project ID.
850
- * @param {string} [config.serviceName='app'] - The name of the Cloud Run service. Defaults to 'app'.
851
- * @param {string} [config.region='europe-west1'] - The Google Cloud region for deployment. Defaults to 'europe-west1'.
852
- * @param {string} config.imageUrl - The URL of the container image to deploy.
853
- * @param {function(object): void} [config.progressCallback] - Optional callback for progress updates. Receives an object with `level` and `data` properties.
854
- * @param {boolean} [config.skipIamCheck=false] - Whether to skip the IAM check.
855
- * @returns {Promise<object>} A promise that resolves with the deployed Cloud Run service object.
856
- * @throws {Error} If deployment fails or required configuration is missing.
857
- */
858
- export async function deployImage({
859
- projectId,
860
- serviceName,
861
- region,
862
- imageUrl,
863
- progressCallback,
864
- skipIamCheck,
865
- }) {
866
- if (!projectId) {
867
- const errorMsg =
868
- 'Error: projectId is required in the configuration object.';
869
- logAndProgress(errorMsg, progressCallback, 'error');
870
- process.exit(1);
871
- }
872
-
873
- if (!imageUrl) {
874
- const errorMsg = 'Error: imageUrl is required in the configuration object.';
875
- logAndProgress(errorMsg, progressCallback, 'error');
876
- if (typeof process !== 'undefined' && process.exit) {
877
- process.exit(1);
878
- } else {
879
- throw new Error(errorMsg);
880
- }
881
- }
882
-
883
- const { v2: CloudRunV2Module } = await import('@google-cloud/run');
884
- const { ServicesClient } = CloudRunV2Module;
885
-
886
- try {
887
- await ensureApisEnabled(
888
- projectId,
889
- REQUIRED_APIS_FOR_IMAGE_DEPLOY,
890
- progressCallback
891
- );
892
-
893
- runClient = new ServicesClient({ projectId });
894
-
895
- logAndProgress(`Project: ${projectId}`, progressCallback);
896
- logAndProgress(`Region: ${region}`, progressCallback);
897
- logAndProgress(`Service Name: ${serviceName}`, progressCallback);
898
- logAndProgress(`Image URL: ${imageUrl}`, progressCallback);
899
-
900
- const service = await deployToCloudRun(
901
- projectId,
902
- region,
903
- serviceName,
904
- imageUrl,
905
- progressCallback,
906
- skipIamCheck
907
- );
908
-
909
- logAndProgress(`Deployment Completed Successfully`, progressCallback);
910
- return service;
911
- } catch (error) {
912
- const deployFailedMessage = `Deployment Failed: ${error.message}`;
913
- console.error(`Deployment Failed`, error);
914
- logAndProgress(deployFailedMessage, progressCallback, 'error');
915
- throw error;
916
- }
917
- }