@google-cloud/cloud-run-mcp 1.5.0 → 1.7.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 (46) hide show
  1. package/.c8rc.json +9 -0
  2. package/.github/workflows/lint-checker.yml +17 -0
  3. package/.github/workflows/npm-publish.yml +1 -1
  4. package/.kokoro/kokoro_build.sh +22 -0
  5. package/.kokoro/presubmit.cfg +21 -0
  6. package/.kokoro/run_tests.sh +31 -0
  7. package/CONTRIBUTING.md +24 -9
  8. package/README.md +52 -11
  9. package/REVIEWING.md +20 -0
  10. package/gemini-extension/GEMINI.md +1 -1
  11. package/lib/cloud-api/billing.js +183 -0
  12. package/lib/cloud-api/build.js +200 -0
  13. package/lib/{gcp-api-helpers.js → cloud-api/helpers.js} +81 -35
  14. package/lib/{gcp-projects.js → cloud-api/projects.js} +58 -28
  15. package/lib/cloud-api/registry.js +98 -0
  16. package/lib/{cloud-run-services.js → cloud-api/run.js} +103 -21
  17. package/lib/cloud-api/storage.js +123 -0
  18. package/lib/deployment/constants.js +54 -0
  19. package/lib/deployment/deployer.js +621 -0
  20. package/lib/deployment/helpers.js +194 -0
  21. package/lib/deployment/source-processor.js +124 -0
  22. package/lib/util/archive.js +104 -0
  23. package/lib/util/helpers.js +42 -0
  24. package/mcp-server.js +21 -10
  25. package/package.json +8 -5
  26. package/test/local/cloud-api/build.test.js +436 -0
  27. package/test/local/cloud-api/projects.test.js +18 -0
  28. package/test/local/deployment-helpers.test.js +231 -0
  29. package/test/local/mcp-server-stdio.test.js +59 -0
  30. package/test/local/mcp-server-streamable-http.test.js +61 -0
  31. package/test/local/notifications.test.js +9 -6
  32. package/test/local/source-processor.test.js +290 -0
  33. package/test/local/test-utils.js +51 -0
  34. package/test/local/tools.test.js +114 -70
  35. package/test/need-gcp/cloud-run-services.test.js +44 -44
  36. package/test/need-gcp/{cloud-run-deploy.test.js → deployer.test.js} +3 -56
  37. package/test/need-gcp/gcp-auth-check.test.js +1 -1
  38. package/test/need-gcp/gcp-projects.test.js +3 -15
  39. package/test/need-gcp/test-helpers.js +175 -0
  40. package/test/need-gcp/workflows/deployment-workflows.test.js +100 -0
  41. package/tools/register-tools.js +106 -97
  42. package/tools/tools.js +2 -9
  43. package/lib/cloud-run-deploy.js +0 -937
  44. package/lib/gcp-billing-accounts.js +0 -98
  45. /package/lib/{gcp-auth-check.js → cloud-api/auth.js} +0 -0
  46. /package/lib/{gcp-metadata.js → cloud-api/metadata.js} +0 -0
@@ -0,0 +1,621 @@
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 '../cloud-api/helpers.js';
18
+ import { zipFiles } from '../util/archive.js';
19
+ import {
20
+ ensureStorageBucketExists,
21
+ uploadToStorageBucket,
22
+ } from '../cloud-api/storage.js';
23
+ import { ensureArtifactRegistryRepoExists } from '../cloud-api/registry.js';
24
+ import { triggerCloudBuild } from '../cloud-api/build.js';
25
+ import { logAndProgress } from '../util/helpers.js';
26
+ import { checkCloudRunServiceExists } from '../cloud-api/run.js';
27
+ import {
28
+ canDeployWithoutBuild,
29
+ createDirectSourceDeploymentContainer,
30
+ makeFileDeploymentMetadata,
31
+ } from './helpers.js';
32
+ import {
33
+ prepareSourceDirectory,
34
+ installDependencies,
35
+ cleanupTempDirectory,
36
+ } from './source-processor.js';
37
+
38
+ import {
39
+ DEPLOYMENT_CONFIG,
40
+ DEPLOYMENT_TYPES,
41
+ MAX_ALLOWED_DIRECT_SOURCE_SIZE_BYTES,
42
+ REQUIRED_APIS,
43
+ } from './constants.js';
44
+
45
+ /**
46
+ * Deploys or updates a Cloud Run service with the specified container image.
47
+ * If the service exists, it's updated; otherwise, a new service is created.
48
+ * The service is configured to be publicly accessible.
49
+ *
50
+ * @async
51
+ * @param {object} context - The context object containing clients and other parameters.
52
+ * @param {string} projectId - The Google Cloud project ID.
53
+ * @param {string} location - The Google Cloud region for the deployment.
54
+ * @param {string} serviceId - The ID for the Cloud Run service.
55
+ * @param {string} imgUrl - The URL of the container image to deploy.
56
+ * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
57
+ * @returns {Promise<object>} A promise that resolves with the Cloud Run service object upon successful deployment or update.
58
+ * @throws {Error} If the deployment or update process fails.
59
+ */
60
+ async function deployToCloudRun(
61
+ context,
62
+ projectId,
63
+ location,
64
+ serviceId,
65
+ imgUrl,
66
+ progressCallback,
67
+ skipIamCheck,
68
+ deploymentType,
69
+ zippedSourceContainer,
70
+ runtime
71
+ ) {
72
+ const parent = context.runClient.locationPath(projectId, location);
73
+ const servicePath = context.runClient.servicePath(
74
+ projectId,
75
+ location,
76
+ serviceId
77
+ );
78
+ const revisionName = `${serviceId}-${Date.now()}`; // Generate a unique revision name
79
+
80
+ const containers =
81
+ deploymentType === DEPLOYMENT_TYPES.NO_BUILD
82
+ ? [zippedSourceContainer]
83
+ : [{ image: imgUrl }];
84
+
85
+ const service = {
86
+ template: {
87
+ revision: revisionName,
88
+ containers,
89
+ },
90
+ labels: {
91
+ 'created-by': DEPLOYMENT_CONFIG.LABEL_CREATED_BY,
92
+ 'deployment-type': deploymentType,
93
+ runtime: runtime,
94
+ },
95
+ };
96
+
97
+ // Conditionally set invokerIamDisabled based on the skipIamCheck flag
98
+ if (skipIamCheck) {
99
+ service.invokerIamDisabled = true;
100
+ }
101
+
102
+ try {
103
+ const exists = await checkCloudRunServiceExists(
104
+ context,
105
+ projectId,
106
+ location,
107
+ serviceId,
108
+ progressCallback
109
+ );
110
+
111
+ // Always perform a dry run first for general validation
112
+ try {
113
+ await logAndProgress(
114
+ `Performing dry run for service ${serviceId}...`,
115
+ progressCallback,
116
+ 'debug'
117
+ );
118
+ const dryRunServiceConfig = JSON.parse(JSON.stringify(service)); // Deep copy for dry run
119
+
120
+ if (exists) {
121
+ dryRunServiceConfig.name = servicePath;
122
+ await callWithRetry(
123
+ () =>
124
+ context.runClient.updateService({
125
+ service: dryRunServiceConfig,
126
+ validateOnly: true,
127
+ }),
128
+ `updateService (dry run) ${serviceId}`
129
+ );
130
+ } else {
131
+ await callWithRetry(
132
+ () =>
133
+ context.runClient.createService({
134
+ parent: parent,
135
+ service: dryRunServiceConfig,
136
+ serviceId: serviceId,
137
+ validateOnly: true,
138
+ }),
139
+ `createService (dry run) ${serviceId}`
140
+ );
141
+ }
142
+ await logAndProgress(
143
+ `Dry run successful for ${serviceId} with current configuration.`,
144
+ progressCallback,
145
+ 'debug'
146
+ );
147
+ } catch (dryRunError) {
148
+ await logAndProgress(
149
+ `Dry run for ${serviceId} failed: ${dryRunError.message}`,
150
+ progressCallback,
151
+ 'warn'
152
+ );
153
+
154
+ // Check if the error is related to invokerIamDisabled (this is a heuristic)
155
+ if (
156
+ skipIamCheck &&
157
+ dryRunError.message &&
158
+ (dryRunError.message.toLowerCase().includes('invokeriamdisabled') ||
159
+ dryRunError.message.toLowerCase().includes('iam policy violation') ||
160
+ dryRunError.code === 3) /* INVALID_ARGUMENT */
161
+ ) {
162
+ await logAndProgress(
163
+ `Dry run suggests 'invokerIamDisabled' is not allowed or invalid. Attempting deployment without it.`,
164
+ progressCallback,
165
+ 'warn'
166
+ );
167
+ delete service.invokerIamDisabled; // Modify the main service object for actual deployment
168
+ } else {
169
+ // For any other validation errors, rethrow to stop the deployment
170
+ const errorMessage = `Dry run validation failed for service ${serviceId}: ${dryRunError.message}`;
171
+ await logAndProgress(errorMessage, progressCallback, 'error');
172
+ throw new Error(errorMessage);
173
+ }
174
+ }
175
+
176
+ let operation;
177
+ if (exists) {
178
+ await logAndProgress(
179
+ `Updating existing service ${serviceId}...`,
180
+ progressCallback
181
+ );
182
+ service.name = servicePath;
183
+ [operation] = await callWithRetry(
184
+ () => context.runClient.updateService({ service }),
185
+ `updateService ${serviceId}`
186
+ );
187
+ } else {
188
+ await logAndProgress(
189
+ `Creating new service ${serviceId}...`,
190
+ progressCallback
191
+ );
192
+ [operation] = await callWithRetry(
193
+ () =>
194
+ context.runClient.createService({
195
+ parent: parent,
196
+ service: service, // 'service' object might have invokerIamDisabled removed
197
+ serviceId: serviceId,
198
+ }),
199
+ `createService ${serviceId}`
200
+ );
201
+ }
202
+
203
+ await logAndProgress(
204
+ `Deploying ${serviceId} to Cloud Run...`,
205
+ progressCallback
206
+ );
207
+ const [response] = await operation.promise();
208
+
209
+ await logAndProgress(
210
+ `Service deployed/updated successfully: ${response.uri}`,
211
+ progressCallback
212
+ );
213
+ return response;
214
+ } catch (error) {
215
+ const errorMessage = `Error deploying/updating service ${serviceId}: ${error.message}`;
216
+ console.error(`Error deploying/updating service ${serviceId}:`, error);
217
+ await logAndProgress(errorMessage, progressCallback, 'error');
218
+ throw error;
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Handles deployment directly from source via a zipped archive (no Cloud Build).
224
+ * This function prepares the source code, installs dependencies, zips the code,
225
+ * uploads it to Google Cloud Storage, and then deploys it to Cloud Run
226
+ * using the direct source deployment mechanism.
227
+ *
228
+ * @param {object} context - The context object containing various GCP clients.
229
+ * @param {string} projectId - The Google Cloud project ID.
230
+ * @param {string} region - The Google Cloud region for the deployment.
231
+ * @param {string} serviceName - The name of the Cloud Run service.
232
+ * @param {Array<string|{filename: string, content: Buffer|string}>} files - Files to deploy.
233
+ * @param {string} bucketName - The name of the GCS bucket to use for staging.
234
+ * @param {object} deploymentAttrs - Deployment attributes detected from the source.
235
+ * @param {string} [deploymentAttrs.runtime] - The detected runtime (e.g., 'nodejs').
236
+ * @param {string[]} [deploymentAttrs.cmd] - The command to run the application.
237
+ * @param {string[]} [deploymentAttrs.args] - The arguments for the command.
238
+ * @param {string} [deploymentAttrs.baseImage] - The base image to use for the deployment.
239
+ * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
240
+ * @param {boolean} [skipIamCheck=false] - Whether to skip the IAM check during Cloud Run service creation/update.
241
+ * @returns {Promise<object>} A promise that resolves with the deployed Cloud Run service object.
242
+ * @throws {Error} If any part of the deployment process fails.
243
+ */
244
+ async function deployWithZip(
245
+ context,
246
+ projectId,
247
+ region,
248
+ serviceName,
249
+ files,
250
+ bucketName,
251
+ deploymentAttrs,
252
+ progressCallback,
253
+ skipIamCheck
254
+ ) {
255
+ await logAndProgress(
256
+ `Attempting direct source deployment...`,
257
+ progressCallback
258
+ );
259
+
260
+ let tempDir;
261
+
262
+ try {
263
+ tempDir = await prepareSourceDirectory(files);
264
+ await installDependencies(tempDir, deploymentAttrs);
265
+
266
+ const archiveBuffer = await zipFiles([tempDir], true, progressCallback);
267
+ const archiveName = DEPLOYMENT_CONFIG.TARGZ_FILE_NAME;
268
+
269
+ if (archiveBuffer.length > MAX_ALLOWED_DIRECT_SOURCE_SIZE_BYTES) {
270
+ const errorMsg = `Warning: Source archive size (${(
271
+ archiveBuffer.length /
272
+ (1024 * 1024)
273
+ ).toFixed(
274
+ 2
275
+ )} MiB) exceeds the ${MAX_ALLOWED_DIRECT_SOURCE_SIZE_BYTES / (1024 * 1024)} MiB limit.`;
276
+ await logAndProgress(errorMsg, progressCallback, 'warn');
277
+ throw new Error('Archive size exceeds limit');
278
+ }
279
+
280
+ const bucket = await ensureStorageBucketExists(
281
+ context,
282
+ bucketName,
283
+ region,
284
+ progressCallback
285
+ );
286
+
287
+ await uploadToStorageBucket(
288
+ context,
289
+ bucket,
290
+ archiveBuffer,
291
+ archiveName,
292
+ progressCallback
293
+ );
294
+ await logAndProgress('Source code uploaded successfully', progressCallback);
295
+
296
+ const container = createDirectSourceDeploymentContainer({
297
+ bucketName,
298
+ fileName: archiveName,
299
+ deploymentAttrs,
300
+ });
301
+
302
+ const service = await deployToCloudRun(
303
+ context,
304
+ projectId,
305
+ region,
306
+ serviceName,
307
+ undefined,
308
+ progressCallback,
309
+ skipIamCheck,
310
+ DEPLOYMENT_TYPES.NO_BUILD,
311
+ container,
312
+ deploymentAttrs.runtime
313
+ );
314
+
315
+ return service;
316
+ } finally {
317
+ await cleanupTempDirectory(tempDir);
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Handles deployment using Google Cloud Build.
323
+ * This function zips the source code, uploads it to Google Cloud Storage,
324
+ * triggers a Cloud Build job to build a container image, pushes the image
325
+ * to Artifact Registry, and then deploys the image to Cloud Run.
326
+ *
327
+ * @param {object} context - The context object containing various GCP clients.
328
+ * @param {string} projectId - The Google Cloud project ID.
329
+ * @param {string} region - The Google Cloud region for the deployment.
330
+ * @param {string} serviceName - The name of the Cloud Run service.
331
+ * @param {Array<string|{filename: string, content: Buffer|string}>} files - Files to deploy.
332
+ * @param {boolean} hasDockerfile - Whether a Dockerfile is present in the files.
333
+ * @param {string} bucketName - The name of the GCS bucket to use for staging.
334
+ * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
335
+ * @param {boolean} [skipIamCheck=false] - Whether to skip the IAM check during Cloud Run service creation/update.
336
+ * @returns {Promise<object>} A promise that resolves with the deployed Cloud Run service object.
337
+ * @throws {Error} If any part of the build or deployment process fails.
338
+ */
339
+ async function deployWithBuild(
340
+ context,
341
+ projectId,
342
+ region,
343
+ serviceName,
344
+ files,
345
+ hasDockerfile,
346
+ bucketName,
347
+ progressCallback,
348
+ skipIamCheck
349
+ ) {
350
+ const imageUrl = `${region}-docker.pkg.dev/${projectId}/${DEPLOYMENT_CONFIG.REPO_NAME}/${serviceName}:${DEPLOYMENT_CONFIG.IMAGE_TAG}`;
351
+
352
+ const bucket = await ensureStorageBucketExists(
353
+ context,
354
+ bucketName,
355
+ region,
356
+ progressCallback
357
+ );
358
+
359
+ const archiveBuffer = await zipFiles(files, false, progressCallback);
360
+ const archiveName = DEPLOYMENT_CONFIG.ZIP_FILE_NAME;
361
+
362
+ await uploadToStorageBucket(
363
+ context,
364
+ bucket,
365
+ archiveBuffer,
366
+ archiveName,
367
+ progressCallback
368
+ );
369
+ await logAndProgress('Source code uploaded successfully', progressCallback);
370
+
371
+ await ensureArtifactRegistryRepoExists(
372
+ context,
373
+ projectId,
374
+ region,
375
+ DEPLOYMENT_CONFIG.REPO_NAME,
376
+ 'DOCKER',
377
+ progressCallback
378
+ );
379
+
380
+ await logAndProgress(`Dockerfile: ${hasDockerfile}`, progressCallback);
381
+
382
+ const buildResult = await triggerCloudBuild(
383
+ context,
384
+ projectId,
385
+ region,
386
+ bucketName,
387
+ archiveName,
388
+ DEPLOYMENT_CONFIG.REPO_NAME,
389
+ imageUrl,
390
+ hasDockerfile,
391
+ progressCallback
392
+ );
393
+
394
+ const builtImageUrl = buildResult.results.images[0].name;
395
+
396
+ const service = await deployToCloudRun(
397
+ context,
398
+ projectId,
399
+ region,
400
+ serviceName,
401
+ builtImageUrl,
402
+ progressCallback,
403
+ skipIamCheck,
404
+ DEPLOYMENT_TYPES.WITH_BUILD
405
+ );
406
+
407
+ await logAndProgress(`Deployment completed successfully`, progressCallback);
408
+ return service;
409
+ }
410
+
411
+ /**
412
+ * Deploys a service to Google Cloud Run.
413
+ * @param {object} config - The deployment configuration.
414
+ * @param {string} config.projectId - The Google Cloud project ID.
415
+ * @param {string} [config.serviceName='app'] - The name of the Cloud Run service. Defaults to 'app'.
416
+ * @param {string} [config.region='europe-west1'] - The Google Cloud region for deployment. Defaults to 'europe-west1'.
417
+ * @param {Array<string|{filename: string, content: Buffer|string}>} config.files - An array of file paths or file objects (with `filename` and `content`) to deploy.
418
+ * @param {function(object): void} [config.progressCallback] - Optional callback for progress updates.
419
+ * @returns {Promise<object>} A promise that resolves with the deployed Cloud Run service object.
420
+ * @throws {Error} If deployment fails or required configuration is missing.
421
+ */
422
+ export async function deploy({
423
+ projectId,
424
+ serviceName,
425
+ region,
426
+ files,
427
+ progressCallback,
428
+ skipIamCheck,
429
+ }) {
430
+ if (!projectId) {
431
+ const errorMsg =
432
+ 'Error: projectId is required in the configuration object.';
433
+ await logAndProgress(errorMsg, progressCallback, 'error');
434
+ throw new Error(errorMsg);
435
+ }
436
+
437
+ if (!serviceName) {
438
+ const errorMsg =
439
+ 'Error: serviceName is required in the configuration object.';
440
+ await logAndProgress(errorMsg, progressCallback, 'error');
441
+ throw new Error(errorMsg);
442
+ }
443
+
444
+ if (!files || !Array.isArray(files) || files.length === 0) {
445
+ const errorMsg =
446
+ 'Error: files array is required in the configuration object.';
447
+ await logAndProgress(errorMsg, progressCallback, 'error');
448
+ if (typeof process !== 'undefined' && process.exit) {
449
+ process.exit(1);
450
+ } else {
451
+ throw new Error(errorMsg);
452
+ }
453
+ }
454
+
455
+ const { Storage } = await import('@google-cloud/storage');
456
+ const { CloudBuildClient } = await import('@google-cloud/cloudbuild');
457
+ const { ArtifactRegistryClient } = await import(
458
+ '@google-cloud/artifact-registry'
459
+ );
460
+ const { v2: CloudRunV2Module } = await import('@google-cloud/run');
461
+ const { ServicesClient } = CloudRunV2Module;
462
+ const { ServiceUsageClient } = await import('@google-cloud/service-usage');
463
+ const { Logging } = await import('@google-cloud/logging');
464
+
465
+ try {
466
+ const context = {
467
+ storage: new Storage({ projectId }),
468
+ cloudBuildClient: new CloudBuildClient({ projectId }),
469
+ artifactRegistryClient: new ArtifactRegistryClient({ projectId }),
470
+ runClient: new ServicesClient({ projectId }),
471
+ serviceUsageClient: new ServiceUsageClient({ projectId }),
472
+ loggingClient: new Logging({ projectId }),
473
+ };
474
+
475
+ await ensureApisEnabled(
476
+ context,
477
+ projectId,
478
+ REQUIRED_APIS.SOURCE_DEPLOY,
479
+ progressCallback
480
+ );
481
+
482
+ const bucketName = `${projectId}-source-bucket`;
483
+
484
+ await logAndProgress(`Project: ${projectId}`, progressCallback);
485
+ await logAndProgress(`Region: ${region}`, progressCallback);
486
+ await logAndProgress(`Service Name: ${serviceName}`, progressCallback);
487
+ await logAndProgress(`Files to deploy: ${files.length}`, progressCallback);
488
+
489
+ // Determines if we can use the direct source deployment (no Cloud Build)
490
+ const { hasDockerfile, deploymentAttrs } =
491
+ makeFileDeploymentMetadata(files);
492
+
493
+ await logAndProgress(`Dockerfile: ${hasDockerfile}`, progressCallback);
494
+
495
+ if (canDeployWithoutBuild({ hasDockerfile, deploymentAttrs })) {
496
+ try {
497
+ return await deployWithZip(
498
+ context,
499
+ projectId,
500
+ region,
501
+ serviceName,
502
+ files,
503
+ bucketName,
504
+ deploymentAttrs,
505
+ progressCallback,
506
+ skipIamCheck
507
+ );
508
+ } catch (error) {
509
+ await logAndProgress(
510
+ `Failed to deploy directly from source: ${error.message}. Retrying to deploy using Cloud Build...`,
511
+ progressCallback,
512
+ 'warn'
513
+ );
514
+ }
515
+ }
516
+
517
+ return await deployWithBuild(
518
+ context,
519
+ projectId,
520
+ region,
521
+ serviceName,
522
+ files,
523
+ hasDockerfile,
524
+ bucketName,
525
+ progressCallback,
526
+ skipIamCheck
527
+ );
528
+ } catch (error) {
529
+ const deployFailedMessage = `Deployment Failed: ${error.message}`;
530
+ console.error(`Deployment Failed`, error);
531
+ await logAndProgress(deployFailedMessage, progressCallback, 'error');
532
+ throw error;
533
+ }
534
+ }
535
+
536
+ /**
537
+ * Deploys a container image to Google Cloud Run.
538
+ * @param {object} config - The deployment configuration.
539
+ * @param {string} config.projectId - The Google Cloud project ID.
540
+ * @param {string} [config.serviceName='app'] - The name of the Cloud Run service. Defaults to 'app'.
541
+ * @param {string} [config.region='europe-west1'] - The Google Cloud region for deployment. Defaults to 'europe-west1'.
542
+ * @param {string} config.imageUrl - The URL of the container image to deploy.
543
+ * @param {function(object): void} [config.progressCallback] - Optional callback for progress updates.
544
+ * @param {boolean} [config.skipIamCheck=false] - Whether to skip the IAM check.
545
+ * @returns {Promise<object>} A promise that resolves with the deployed Cloud Run service object.
546
+ * @throws {Error} If deployment fails or required configuration is missing.
547
+ */
548
+ export async function deployImage({
549
+ projectId,
550
+ serviceName,
551
+ region,
552
+ imageUrl,
553
+ progressCallback,
554
+ skipIamCheck,
555
+ }) {
556
+ if (!projectId) {
557
+ const errorMsg =
558
+ 'Error: projectId is required in the configuration object.';
559
+ await logAndProgress(errorMsg, progressCallback, 'error');
560
+ throw new Error(errorMsg);
561
+ }
562
+
563
+ if (!serviceName) {
564
+ const errorMsg =
565
+ 'Error: serviceName is required in the configuration object.';
566
+ await logAndProgress(errorMsg, progressCallback, 'error');
567
+ throw new Error(errorMsg);
568
+ }
569
+
570
+ if (!imageUrl) {
571
+ const errorMsg = 'Error: imageUrl is required in the configuration object.';
572
+ await logAndProgress(errorMsg, progressCallback, 'error');
573
+ if (typeof process !== 'undefined' && process.exit) {
574
+ process.exit(1);
575
+ } else {
576
+ throw new Error(errorMsg);
577
+ }
578
+ }
579
+
580
+ const { v2: CloudRunV2Module } = await import('@google-cloud/run');
581
+ const { ServicesClient } = CloudRunV2Module;
582
+ const { ServiceUsageClient } = await import('@google-cloud/service-usage');
583
+
584
+ try {
585
+ const context = {
586
+ runClient: new ServicesClient({ projectId }),
587
+ serviceUsageClient: new ServiceUsageClient({ projectId }),
588
+ };
589
+
590
+ await ensureApisEnabled(
591
+ context,
592
+ projectId,
593
+ REQUIRED_APIS.IMAGE_DEPLOY,
594
+ progressCallback
595
+ );
596
+
597
+ await logAndProgress(`Project: ${projectId}`, progressCallback);
598
+ await logAndProgress(`Region: ${region}`, progressCallback);
599
+ await logAndProgress(`Service Name: ${serviceName}`, progressCallback);
600
+ await logAndProgress(`Image URL: ${imageUrl}`, progressCallback);
601
+
602
+ const service = await deployToCloudRun(
603
+ context,
604
+ projectId,
605
+ region,
606
+ serviceName,
607
+ imageUrl,
608
+ progressCallback,
609
+ skipIamCheck,
610
+ DEPLOYMENT_TYPES.IMAGE
611
+ );
612
+
613
+ await logAndProgress(`Deployment completed successfully`, progressCallback);
614
+ return service;
615
+ } catch (error) {
616
+ const deployFailedMessage = `Deployment Failed: ${error.message}`;
617
+ console.error(`Deployment Failed`, error);
618
+ await logAndProgress(deployFailedMessage, progressCallback, 'error');
619
+ throw error;
620
+ }
621
+ }