@google-cloud/cloud-run-mcp 1.0.0 → 1.3.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.
@@ -0,0 +1,17 @@
1
+ name: Run Local Tests
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+ branches: [ main ]
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-node@v4
14
+ with:
15
+ node-version: 20
16
+ - run: npm ci
17
+ - run: npm test
@@ -0,0 +1,23 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ release:
5
+ types: [created]
6
+
7
+ jobs:
8
+ publish-npm:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-node@v4
13
+ with:
14
+ node-version: 20
15
+ - run: npm ci
16
+ # Now configure with the publish service for install.
17
+ - uses: actions/setup-node@v4
18
+ with:
19
+ node-version: 20
20
+ registry-url: https://wombat-dressing-room.appspot.com/
21
+ - run: npm publish
22
+ env:
23
+ NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}}
package/CONTRIBUTING.md CHANGED
@@ -105,3 +105,17 @@ npm run test:deploy
105
105
  ```
106
106
 
107
107
  This script requires an existing Google Cloud Project ID to be provided when prompted or as a command-line argument.
108
+
109
+ ## Publishing to npm
110
+
111
+ To update the [npm package]https://www.npmjs.com/package/@google-cloud/cloud-run-mcp)
112
+
113
+ Run the following:
114
+
115
+ * `git checkout -b new-release`
116
+ * `npm test`
117
+ * `npm version minor`
118
+ * `git push --follow-tags`
119
+ * On GitHub, [create a new release](https://github.com/GoogleCloudPlatform/cloud-run-mcp/releases/new) for this tag.
120
+
121
+ Then a [GitHub Action]https://github.com/GoogleCloudPlatform/cloud-run-mcp/blob/main/.github/workflows/npm-publish.yml) automatically publishes to npm when new releases are pushes.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Enable MCP-compatible AI agents to deploy apps to Cloud Run.
6
6
  "mcpServers":{
7
7
  "cloud-run": {
8
8
  "command": "npx",
9
- "args": ["-y", "https://github.com/GoogleCloudPlatform/cloud-run-mcp"]
9
+ "args": ["-y", "@google-cloud/cloud-run-mcp"]
10
10
  }
11
11
  }
12
12
  ```
@@ -72,7 +72,7 @@ Then configure the MCP server using either Node.js or Docker:
72
72
  ```json
73
73
  "cloud-run": {
74
74
  "command": "npx",
75
- "args": ["-y", "https://github.com/GoogleCloudPlatform/cloud-run-mcp"]
75
+ "args": ["-y", "@google-cloud/cloud-run-mcp"]
76
76
  }
77
77
  ```
78
78
 
@@ -81,7 +81,7 @@ Then configure the MCP server using either Node.js or Docker:
81
81
  ```json
82
82
  "cloud-run": {
83
83
  "command": "npx",
84
- "args": ["-y", "https://github.com/GoogleCloudPlatform/cloud-run-mcp"],
84
+ "args": ["-y", "@google-cloud/cloud-run-mcp"],
85
85
  "env": {
86
86
  "GOOGLE_CLOUD_PROJECT": "PROJECT_NAME",
87
87
  "GOOGLE_CLOUD_REGION": "PROJECT_REGION",
@@ -118,13 +118,14 @@ See Docker's [MCP catalog](https://hub.docker.com/mcp/server/cloud-run-mcp/overv
118
118
  }
119
119
  ```
120
120
 
121
- ## Use as a `gemini-cli` extension
121
+ ## Use as a Gemini CLI extension
122
122
 
123
- To install this as a `gemini-cli` extension, run the following command:
123
+ To install this as a [Gemini CLI](https://github.com/google-gemini/gemini-cli) extension, run the following command:
124
124
 
125
125
  ```bash
126
- mkdir -p ~/.gemini/extensions/cloud-run && \
127
- curl -s -L https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/main/gemini-extension.json > ~/.gemini/extensions/cloud-run/gemini-extension.json
126
+ mkdir -p ~/.gemini/extensions/cloud-run/gemini-extension && \
127
+ curl -s -L https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/main/gemini-extension.json > ~/.gemini/extensions/cloud-run/gemini-extension.json && \
128
+ curl -s -L https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/main/gemini-extension/Gemini.md > ~/.gemini/extensions/cloud-run/gemini-extension/Gemini.md
128
129
  ```
129
130
 
130
131
  ## Use as remote MCP server
@@ -0,0 +1,57 @@
1
+ # Cloud Run MCP Server
2
+
3
+ ## Code that can be deployed
4
+
5
+ Only web servers can be deployed using this MCP server.
6
+ The code needs to listen for HTTP reqeusts on the port defined by the $PORT environment variable or 8080.
7
+
8
+ ### Supported languages
9
+
10
+ - If the code is in Node.js, Python, Go, Java, .NET, PHP, Ruby, a Dockerfile is not needed.
11
+ - If the code is in another language, or has any custom dependency needs, a Dockerfile is needed.
12
+
13
+ ### Static-only apps
14
+
15
+ To deploy static-only applications, create a Dockerfile that serves these static files. For example using `nginx`:
16
+
17
+ `Dockerfile`
18
+
19
+ ```
20
+ FROM nginx:stable
21
+
22
+ COPY ./static /var/www
23
+ COPY ./nginx.conf /etc/nginx/conf.d/default.conf
24
+
25
+ CMD ["nginx", "-g", "daemon off;"]
26
+ ```
27
+
28
+ `nginx.conf`:
29
+
30
+ ```
31
+ server {
32
+ listen 8080;
33
+ server_name _;
34
+
35
+ root /var/www/;
36
+ index index.html;
37
+
38
+ # Force all paths to load either itself (js files) or go through index.html.
39
+ location / {
40
+ try_files $uri /index.html;
41
+ }
42
+ }
43
+ ```
44
+
45
+ ## Google Cloud pre-requisities
46
+
47
+ The user must have an existing Google Cloud account with billing set up, and ideally an existing Google Cloud project.
48
+
49
+ If deployment fails because of an access or IAM error, it is likely that the user doesn't have Google Cloud credentials on the local machine.
50
+ The user mush follow these steps:
51
+
52
+ 1. Install the [Google Cloud SDK](https://cloud.google.com/sdk/docs/install) and authenticate with their Google account.
53
+
54
+ 2. Set up application credentials using the command:
55
+ ```bash
56
+ gcloud auth application-default login
57
+ ```
@@ -4,7 +4,8 @@
4
4
  "mcpServers": {
5
5
  "cloud-run": {
6
6
  "command": "npx",
7
- "args": ["-y", "https://github.com/GoogleCloudPlatform/cloud-run-mcp"]
7
+ "args": ["-y", "@google-cloud/cloud-run-mcp"]
8
8
  }
9
- }
9
+ },
10
+ "contextFileName": "gemini-extension/GEMINI.md"
10
11
  }
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
16
 
17
+ import { callWithRetry, ensureApisEnabled } from './gcp-api-helpers.js';
18
+
17
19
  // Configuration
18
20
  const REPO_NAME = 'mcp-cloud-run-deployments';
19
21
  const ZIP_FILE_NAME = 'source.zip';
@@ -63,50 +65,6 @@ function logAndProgress(message, progressCallback, severity = 'info') {
63
65
  }
64
66
  }
65
67
 
66
- /**
67
- * Ensures that the specified Google Cloud APIs are enabled for the given project.
68
- * If an API is not enabled, it attempts to enable it.
69
- * Throws an error if an API cannot be enabled.
70
- *
71
- * @async
72
- * @param {string} projectId - The Google Cloud project ID.
73
- * @param {string[]} apis - An array of API identifiers to check and enable (e.g., 'run.googleapis.com').
74
- * @param {function(string, string=): void} progressCallback - A function to call with progress updates.
75
- * The first argument is the message, the optional second argument is the type ('error', 'warning', 'info').
76
- * @throws {Error} If an API fails to enable or if there's an issue checking its status.
77
- * @returns {Promise<void>} A promise that resolves when all specified APIs are enabled.
78
- */
79
- async function ensureApisEnabled(projectId, apis, progressCallback) {
80
- const { ServiceUsageClient } = await import('@google-cloud/service-usage');
81
- const serviceUsageClient = new ServiceUsageClient({ projectId });
82
- logAndProgress('Checking and enabling required APIs...', progressCallback);
83
-
84
- for (const api of apis) {
85
- const serviceName = `projects/${projectId}/services/${api}`;
86
- try {
87
- const [service] = await serviceUsageClient.getService({
88
- name: serviceName,
89
- });
90
- if (service.state !== 'ENABLED') {
91
- logAndProgress(
92
- `API [${api}] is not enabled. Enabling...`,
93
- progressCallback
94
- );
95
- const [operation] = await serviceUsageClient.enableService({
96
- name: serviceName,
97
- });
98
- await operation.promise();
99
- }
100
- } catch (error) {
101
- const errorMessage = `Failed to ensure API [${api}] is enabled. Please check manually.`;
102
- console.error(errorMessage, error);
103
- logAndProgress(errorMessage, progressCallback, 'error');
104
- throw new Error(errorMessage);
105
- }
106
- }
107
- logAndProgress('All required APIs are enabled.', progressCallback);
108
- }
109
-
110
68
  /**
111
69
  * Checks if a Cloud Run service already exists.
112
70
  *
@@ -127,7 +85,10 @@ async function checkCloudRunServiceExists(
127
85
  const parent = runClient.locationPath(projectId, location);
128
86
  const servicePath = runClient.servicePath(projectId, location, serviceId);
129
87
  try {
130
- await runClient.getService({ name: servicePath });
88
+ await callWithRetry(
89
+ () => runClient.getService({ name: servicePath }),
90
+ `getService ${serviceId}`
91
+ );
131
92
  logAndProgress(
132
93
  `Cloud Run service ${serviceId} already exists.`,
133
94
  progressCallback
@@ -208,17 +169,25 @@ async function deployToCloudRun(
208
169
 
209
170
  if (exists) {
210
171
  dryRunServiceConfig.name = servicePath;
211
- await runClient.updateService({
212
- service: dryRunServiceConfig,
213
- validateOnly: true,
214
- });
172
+ await callWithRetry(
173
+ () =>
174
+ runClient.updateService({
175
+ service: dryRunServiceConfig,
176
+ validateOnly: true,
177
+ }),
178
+ `updateService (dry run) ${serviceId}`
179
+ );
215
180
  } else {
216
- await runClient.createService({
217
- parent: parent,
218
- service: dryRunServiceConfig,
219
- serviceId: serviceId,
220
- validateOnly: true,
221
- });
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
+ );
222
191
  }
223
192
  logAndProgress(
224
193
  `Dry run successful for ${serviceId} with current configuration.`,
@@ -261,14 +230,21 @@ async function deployToCloudRun(
261
230
  progressCallback
262
231
  );
263
232
  service.name = servicePath;
264
- [operation] = await runClient.updateService({ service });
233
+ [operation] = await callWithRetry(
234
+ () => runClient.updateService({ service }),
235
+ `updateService ${serviceId}`
236
+ );
265
237
  } else {
266
238
  logAndProgress(`Creating new service ${serviceId}...`, progressCallback);
267
- [operation] = await runClient.createService({
268
- parent: parent,
269
- service: service, // 'service' object might have invokerIamDisabled removed
270
- serviceId: serviceId,
271
- });
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
+ );
272
248
  }
273
249
 
274
250
  logAndProgress(`Deploying ${serviceId} to Cloud Run...`, progressCallback);
@@ -305,7 +281,10 @@ async function ensureStorageBucketExists(
305
281
  ) {
306
282
  const bucket = storage.bucket(bucketName);
307
283
  try {
308
- const [exists] = await bucket.exists();
284
+ const [exists] = await callWithRetry(
285
+ () => bucket.exists(),
286
+ `storage.bucket.exists ${bucketName}`
287
+ );
309
288
  if (exists) {
310
289
  logAndProgress(`Bucket ${bucketName} already exists.`, progressCallback);
311
290
  return bucket;
@@ -315,9 +294,10 @@ async function ensureStorageBucketExists(
315
294
  progressCallback
316
295
  );
317
296
  try {
318
- const [createdBucket] = await storage.createBucket(bucketName, {
319
- location: location,
320
- });
297
+ const [createdBucket] = await callWithRetry(
298
+ () => storage.createBucket(bucketName, { location: location }),
299
+ `storage.createBucket ${bucketName}`
300
+ );
321
301
  logAndProgress(
322
302
  `Storage bucket ${createdBucket.name} created successfully in ${location}.`,
323
303
  progressCallback
@@ -441,7 +421,10 @@ async function uploadToStorageBucket(
441
421
  `Uploading buffer to gs://${bucket.name}/${destinationBlobName}...`,
442
422
  progressCallback
443
423
  );
444
- await bucket.file(destinationBlobName).save(buffer);
424
+ await callWithRetry(
425
+ () => bucket.file(destinationBlobName).save(buffer),
426
+ `storage.bucket.file.save ${destinationBlobName}`
427
+ );
445
428
  logAndProgress(
446
429
  `File ${destinationBlobName} uploaded successfully to gs://${bucket.name}/${destinationBlobName}.`,
447
430
  progressCallback
@@ -483,9 +466,10 @@ async function ensureArtifactRegistryRepoExists(
483
466
  );
484
467
 
485
468
  try {
486
- const [repository] = await artifactRegistryClient.getRepository({
487
- name: repoPath,
488
- });
469
+ const [repository] = await callWithRetry(
470
+ () => artifactRegistryClient.getRepository({ name: repoPath }),
471
+ `artifactRegistry.getRepository ${repositoryId}`
472
+ );
489
473
  logAndProgress(
490
474
  `Repository ${repositoryId} already exists in ${location}.`,
491
475
  progressCallback
@@ -500,31 +484,26 @@ async function ensureArtifactRegistryRepoExists(
500
484
  const repositoryToCreate = {
501
485
  format: format,
502
486
  };
503
- try {
504
- const [operation] = await artifactRegistryClient.createRepository({
505
- parent: parent,
506
- repository: repositoryToCreate,
507
- repositoryId: repositoryId,
508
- });
509
- logAndProgress(
510
- `Creating Artifact Registry repository ${repositoryId}...`,
511
- progressCallback
512
- );
513
- const [result] = await operation.promise();
514
- logAndProgress(
515
- `Artifact Registry repository ${result.name} created successfully.`,
516
- progressCallback
517
- );
518
- return result;
519
- } catch (createError) {
520
- const errorMessage = `Failed to create Artifact Registry repository ${repositoryId}. Error details: ${createError.message}`;
521
- console.error(
522
- `Failed to create Artifact Registry repository ${repositoryId}. Error details:`,
523
- createError
524
- );
525
- logAndProgress(errorMessage, progressCallback, 'error');
526
- throw createError;
527
- }
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;
528
507
  } else {
529
508
  const errorMessage = `Error checking/creating repository ${repositoryId}: ${error.message}`;
530
509
  console.error(
@@ -605,19 +584,19 @@ async function triggerCloudBuild(
605
584
  `Initiating Cloud Build for gs://${sourceBucketName}/${sourceBlobName} in ${location}...`,
606
585
  progressCallback
607
586
  );
608
- const [operation] = await cloudBuildClient.createBuild({
609
- projectId: projectId,
610
- build: build,
611
- });
587
+ const [operation] = await callWithRetry(
588
+ () => cloudBuildClient.createBuild({ projectId: projectId, build: build }),
589
+ 'cloudBuild.createBuild'
590
+ );
612
591
 
613
592
  logAndProgress(`Cloud Build job started...`, progressCallback);
614
593
  const buildId = operation.metadata.build.id;
615
594
  let completedBuild;
616
595
  while (true) {
617
- const [getBuildOperation] = await cloudBuildClient.getBuild({
618
- projectId: projectId,
619
- id: buildId,
620
- });
596
+ const [getBuildOperation] = await callWithRetry(
597
+ () => cloudBuildClient.getBuild({ projectId: projectId, id: buildId }),
598
+ `cloudBuild.getBuild ${buildId}`
599
+ );
621
600
  if (
622
601
  ['SUCCESS', 'FAILURE', 'INTERNAL_ERROR', 'TIMEOUT', 'CANCELLED'].includes(
623
602
  getBuildOperation.status
@@ -668,11 +647,15 @@ async function triggerCloudBuild(
668
647
  );
669
648
 
670
649
  // Fetch the most recent N log entries
671
- const [entries] = await loggingClient.getEntries({
672
- filter: logFilter,
673
- orderBy: 'timestamp desc', // Get latest logs first
674
- pageSize: BUILD_LOGS_LINES_TO_FETCH,
675
- });
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
+ );
676
659
 
677
660
  if (entries && entries.length > 0) {
678
661
  // Entries are newest first, reverse for chronological order of the snippet
@@ -15,6 +15,7 @@ limitations under the License.
15
15
  */
16
16
 
17
17
  import protofiles from 'google-proto-files';
18
+ import { callWithRetry, ensureApisEnabled } from './gcp-api-helpers.js';
18
19
 
19
20
  let runClient;
20
21
  let loggingClient;
@@ -32,13 +33,19 @@ export async function listServices(projectId, location) {
32
33
  const { ServicesClient } = v2;
33
34
  runClient = new ServicesClient({ projectId });
34
35
  }
36
+
37
+ await ensureApisEnabled(projectId, ['run.googleapis.com']);
38
+
35
39
  const parent = runClient.locationPath(projectId, location);
36
40
 
37
41
  try {
38
42
  console.log(
39
43
  `Listing Cloud Run services in project ${projectId}, location ${location}...`
40
44
  );
41
- const [services] = await runClient.listServices({ parent });
45
+ const [services] = await callWithRetry(
46
+ () => runClient.listServices({ parent }),
47
+ 'listServices'
48
+ );
42
49
  return services;
43
50
  } catch (error) {
44
51
  console.error(`Error listing Cloud Run services:`, error);
@@ -66,7 +73,10 @@ export async function getService(projectId, location, serviceId) {
66
73
  console.log(
67
74
  `Getting details for Cloud Run service ${serviceId} in project ${projectId}, location ${location}...`
68
75
  );
69
- const [service] = await runClient.getService({ name: servicePath });
76
+ const [service] = await callWithRetry(
77
+ () => runClient.getService({ name: servicePath }),
78
+ 'getService'
79
+ );
70
80
  return service;
71
81
  } catch (error) {
72
82
  console.error(
@@ -123,8 +133,10 @@ export async function getServiceLogs(
123
133
  console.log(`Request options: ${JSON.stringify(options)}`);
124
134
 
125
135
  // getEntries returns the entries and the full API response
126
- const [entries, nextRequestOptions, apiResponse] =
127
- await loggingClient.getEntries(options);
136
+ const [entries, nextRequestOptions, apiResponse] = await callWithRetry(
137
+ () => loggingClient.getEntries(options),
138
+ 'getEntries'
139
+ );
128
140
 
129
141
  const formattedLogLines = entries
130
142
  .map((entry) => formatLogEntry(entry))
@@ -0,0 +1,143 @@
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
+ /**
18
+ * Calls a function with retry logic for GCP API calls.
19
+ * Retries on gRPC error code 7 (PERMISSION_DENIED).
20
+ * @param {Function} fn The function to call.
21
+ * @param {string} description A description of the function being called, for logging.
22
+ * @returns {Promise<any>} The result of the function.
23
+ */
24
+ export async function callWithRetry(fn, description) {
25
+ const maxRetries = 7;
26
+ const initialBackoff = 1000; // 1 second
27
+ let retries = 0;
28
+
29
+ while (true) {
30
+ try {
31
+ return await fn();
32
+ } catch (error) {
33
+ if (error.code === 7 && retries < maxRetries) {
34
+ retries++;
35
+ let backoff;
36
+ if (retries === 1) {
37
+ backoff = 15000; // 15 seconds for the first retry
38
+ } else {
39
+ backoff = initialBackoff * Math.pow(2, retries - 2);
40
+ }
41
+ console.warn(
42
+ `API call "${description}" failed with PERMISSION_DENIED. Retrying in ${
43
+ backoff / 1000
44
+ }s... (attempt ${retries}/${maxRetries})`
45
+ );
46
+ await new Promise((resolve) => setTimeout(resolve, backoff));
47
+ } else {
48
+ throw error;
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Checks if a single Google Cloud API is enabled and enables it if not.
56
+ *
57
+ * @param {object} serviceUsageClient - The Service Usage client.
58
+ * @param {string} serviceName - The full name of the service (e.g., 'projects/my-project/services/run.googleapis.com').
59
+ * @param {string} api - The API identifier (e.g., 'run.googleapis.com').
60
+ * @param {function(string, string=): void} progressCallback - A function to call with progress updates.
61
+ * @returns {Promise<void>} A promise that resolves when the API is enabled.
62
+ * @throws {Error} If the API fails to enable or if there's an issue checking its status.
63
+ */
64
+ async function checkAndEnableApi(
65
+ serviceUsageClient,
66
+ serviceName,
67
+ api,
68
+ progressCallback
69
+ ) {
70
+ const [service] = await callWithRetry(
71
+ () => serviceUsageClient.getService({ name: serviceName }),
72
+ `getService ${api}`
73
+ );
74
+ if (service.state !== 'ENABLED') {
75
+ const message = `API [${api}] is not enabled. Enabling...`;
76
+ console.log(message);
77
+ if (progressCallback) progressCallback({ level: 'info', data: message });
78
+
79
+ const [operation] = await callWithRetry(
80
+ () => serviceUsageClient.enableService({ name: serviceName }),
81
+ `enableService ${api}`
82
+ );
83
+ await operation.promise();
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Ensures that the specified Google Cloud APIs are enabled for the given project.
89
+ * If an API is not enabled, it attempts to enable it. Retries any failure once after 1s.
90
+ * Throws an error if an API cannot be enabled.
91
+ *
92
+ * @async
93
+ * @param {string} projectId - The Google Cloud project ID.
94
+ * @param {string[]} apis - An array of API identifiers to check and enable (e.g., 'run.googleapis.com').
95
+ * @param {function(string, string=): void} progressCallback - A function to call with progress updates.
96
+ * The first argument is the message, the optional second argument is the type ('error', 'warning', 'info').
97
+ * @throws {Error} If an API fails to enable or if there's an issue checking its status.
98
+ * @returns {Promise<void>} A promise that resolves when all specified APIs are enabled.
99
+ */
100
+ export async function ensureApisEnabled(projectId, apis, progressCallback) {
101
+ const { ServiceUsageClient } = await import('@google-cloud/service-usage');
102
+ const serviceUsageClient = new ServiceUsageClient({ projectId });
103
+ const message = 'Checking and enabling required APIs...';
104
+ console.log(message);
105
+ if (progressCallback) progressCallback({ level: 'info', data: message });
106
+
107
+
108
+ for (const api of apis) {
109
+ const serviceName = `projects/${projectId}/services/${api}`;
110
+ try {
111
+ await checkAndEnableApi(
112
+ serviceUsageClient,
113
+ serviceName,
114
+ api,
115
+ progressCallback
116
+ );
117
+ } catch (error) {
118
+ // First attempt failed, log a warning and retry once after a delay.
119
+ const warnMsg = `Failed to check/enable ${api}, retrying in 1s...`;
120
+ console.warn(warnMsg);
121
+ if (progressCallback) progressCallback({ level: 'warn', data: warnMsg });
122
+
123
+ await new Promise((resolve) => setTimeout(resolve, 1000));
124
+ try {
125
+ await checkAndEnableApi(
126
+ serviceUsageClient,
127
+ serviceName,
128
+ api,
129
+ progressCallback
130
+ );
131
+ } catch (retryError) {
132
+ // If the retry also fails, throw an error.
133
+ const errorMessage = `Failed to ensure API [${api}] is enabled after retry. Please check manually.`;
134
+ console.error(errorMessage, retryError);
135
+ if (progressCallback) progressCallback({ level: 'error', data: errorMessage });
136
+ throw new Error(errorMessage);
137
+ }
138
+ }
139
+ }
140
+ const successMsg = 'All required APIs are enabled.';
141
+ console.log(successMsg);
142
+ if (progressCallback) progressCallback({ level: 'info', data: successMsg });
143
+ }