@google-cloud/cloud-run-mcp 1.6.0 → 1.8.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.
@@ -2,7 +2,7 @@ name: Publish to npm
2
2
 
3
3
  on:
4
4
  release:
5
- types: [created]
5
+ types: [published]
6
6
 
7
7
  jobs:
8
8
  publish-npm:
package/CONTRIBUTING.md CHANGED
@@ -126,7 +126,7 @@ This script requires an existing Google Cloud Project ID to be provided when pro
126
126
 
127
127
  ## Publishing to npm
128
128
 
129
- To update the [npm package]https://www.npmjs.com/package/@google-cloud/cloud-run-mcp)
129
+ To update the [npm package](https://www.npmjs.com/package/@google-cloud/cloud-run-mcp)
130
130
 
131
131
  Run the following:
132
132
 
package/lib/clients.js ADDED
@@ -0,0 +1,222 @@
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
+ const AUTHORIZATION_HEADER = 'Authorization';
18
+ const BEARER_PREFIX = 'Bearer';
19
+ const GCLOUD_AUTH = 'gcloud_auth';
20
+
21
+ function keyGenerator(projectId, accessToken) {
22
+ return accessToken !== GCLOUD_AUTH ? projectId + accessToken : projectId;
23
+ }
24
+
25
+ const clients = {
26
+ run: new Map(),
27
+ builds: new Map(),
28
+ serviceUsage: new Map(),
29
+ storage: new Map(),
30
+ cloudBuild: new Map(),
31
+ artifactRegistry: new Map(),
32
+ logging: new Map(),
33
+ billing: new Map(),
34
+ projects: new Map(),
35
+ };
36
+
37
+ function getAuthClient(accessToken) {
38
+ return {
39
+ getRequestHeaders: () => {
40
+ const headers = new Map();
41
+ headers.set(AUTHORIZATION_HEADER, `${BEARER_PREFIX} ${accessToken}`);
42
+ return headers;
43
+ },
44
+ };
45
+ }
46
+
47
+ export async function getClient(
48
+ service,
49
+ key,
50
+ loadClient,
51
+ options = {},
52
+ accessToken = null
53
+ ) {
54
+ if (!clients[service].has(key)) {
55
+ const ClientClass = await loadClient();
56
+ const finalOptions = { ...options };
57
+ if (accessToken && accessToken !== GCLOUD_AUTH) {
58
+ finalOptions.authClient = getAuthClient(accessToken);
59
+ }
60
+ clients[service].set(key, new ClientClass(finalOptions));
61
+ }
62
+ return clients[service].get(key);
63
+ }
64
+
65
+ /**
66
+ * Gets a Cloud Run Services Client for the specified project.
67
+ * @param {string} projectId - The Google Cloud project ID.
68
+ * @returns {Promise<import('@google-cloud/run').v2.ServicesClient>}
69
+ */
70
+ export async function getRunClient(projectId, accessToken = GCLOUD_AUTH) {
71
+ const key = keyGenerator(projectId, accessToken);
72
+ return getClient(
73
+ 'run',
74
+ key,
75
+ async () => (await import('@google-cloud/run')).v2.ServicesClient,
76
+ { projectId },
77
+ accessToken
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Gets a Cloud Run Build Client for the specified project.
83
+ * @param {string} projectId - The Google Cloud project ID.
84
+ * @returns {Promise<import('@google-cloud/run').v2.BuildsClient>}
85
+ */
86
+ export async function getBuildsClient(projectId, accessToken = GCLOUD_AUTH) {
87
+ const key = keyGenerator(projectId, accessToken);
88
+ return getClient(
89
+ 'builds',
90
+ key,
91
+ async () => (await import('@google-cloud/run')).v2.BuildsClient,
92
+ { projectId },
93
+ accessToken
94
+ );
95
+ }
96
+
97
+ /**
98
+ * Gets a Service Usage Client for the specified project.
99
+ * @param {string} projectId - The Google Cloud project ID.
100
+ * @returns {Promise<import('@google-cloud/service-usage').ServiceUsageClient>}
101
+ */
102
+ export async function getServiceUsageClient(
103
+ projectId,
104
+ accessToken = GCLOUD_AUTH
105
+ ) {
106
+ const key = keyGenerator(projectId, accessToken);
107
+ return getClient(
108
+ 'serviceUsage',
109
+ key,
110
+ async () =>
111
+ (await import('@google-cloud/service-usage')).ServiceUsageClient,
112
+ { projectId },
113
+ accessToken
114
+ );
115
+ }
116
+
117
+ /**
118
+ * Gets a Storage Client for the specified project.
119
+ * @param {string} projectId - The Google Cloud project ID.
120
+ * @returns {Promise<import('@google-cloud/storage').Storage>}
121
+ */
122
+ export async function getStorageClient(projectId, accessToken = GCLOUD_AUTH) {
123
+ const key = keyGenerator(projectId, accessToken);
124
+ return getClient(
125
+ 'storage',
126
+ key,
127
+ async () => (await import('@google-cloud/storage')).Storage,
128
+ { projectId },
129
+ accessToken
130
+ );
131
+ }
132
+
133
+ /**
134
+ * Gets a Cloud Build Client for the specified project.
135
+ * @param {string} projectId - The Google Cloud project ID.
136
+ * @returns {Promise<import('@google-cloud/cloudbuild').CloudBuildClient>}
137
+ */
138
+ export async function getCloudBuildClient(
139
+ projectId,
140
+ accessToken = GCLOUD_AUTH
141
+ ) {
142
+ const key = keyGenerator(projectId, accessToken);
143
+ return getClient(
144
+ 'cloudBuild',
145
+ key,
146
+ async () => (await import('@google-cloud/cloudbuild')).CloudBuildClient,
147
+ { projectId },
148
+ accessToken
149
+ );
150
+ }
151
+
152
+ /**
153
+ * Gets an Artifact Registry Client for the specified project.
154
+ * @param {string} projectId - The Google Cloud project ID.
155
+ * @returns {Promise<import('@google-cloud/artifact-registry').ArtifactRegistryClient>}
156
+ */
157
+ export async function getArtifactRegistryClient(
158
+ projectId,
159
+ accessToken = GCLOUD_AUTH
160
+ ) {
161
+ const key = keyGenerator(projectId, accessToken);
162
+ return getClient(
163
+ 'artifactRegistry',
164
+ key,
165
+ async () =>
166
+ (await import('@google-cloud/artifact-registry')).ArtifactRegistryClient,
167
+ { projectId },
168
+ accessToken
169
+ );
170
+ }
171
+
172
+ /**
173
+ * Gets a Logging Client for the specified project.
174
+ * @param {string} projectId - The Google Cloud project ID.
175
+ * @returns {Promise<import('@google-cloud/logging').Logging>}
176
+ */
177
+ export async function getLoggingClient(projectId, accessToken = GCLOUD_AUTH) {
178
+ const key = keyGenerator(projectId, accessToken);
179
+ return getClient(
180
+ 'logging',
181
+ key,
182
+ async () => (await import('@google-cloud/logging')).Logging,
183
+ { projectId },
184
+ accessToken
185
+ );
186
+ }
187
+
188
+ /**
189
+ * Gets a Billing Client for the specified project.
190
+ * Note: BillingClient usually doesn't take projectId in constructor for listing accounts,
191
+ * but might for project billing info. We will cache by projectId anyway or 'global' if projectId is null.
192
+ * @param {string} [projectId] - The Google Cloud project ID (optional).
193
+ * @returns {Promise<import('@google-cloud/billing').CloudBillingClient>}
194
+ */
195
+ export async function getBillingClient(
196
+ projectId = 'global',
197
+ accessToken = GCLOUD_AUTH
198
+ ) {
199
+ const key = keyGenerator(projectId, accessToken);
200
+ return getClient(
201
+ 'billing',
202
+ key,
203
+ async () => (await import('@google-cloud/billing')).CloudBillingClient,
204
+ projectId !== 'global' ? { projectId } : {},
205
+ accessToken
206
+ );
207
+ }
208
+
209
+ /**
210
+ * Gets a Projects Client (Resource Manager).
211
+ * @returns {Promise<import('@google-cloud/resource-manager').ProjectsClient>}
212
+ */
213
+ export async function getProjectsClient(accessToken = GCLOUD_AUTH) {
214
+ const key = keyGenerator('global', accessToken);
215
+ return getClient(
216
+ 'projects',
217
+ key,
218
+ async () => (await import('@google-cloud/resource-manager')).ProjectsClient,
219
+ {},
220
+ accessToken
221
+ );
222
+ }
@@ -14,6 +14,15 @@ See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
16
 
17
+ /**
18
+ * Lists all accessible Google Cloud Billing Accounts.
19
+ * @async
20
+ * @function listBillingAccounts
21
+ * @returns {Promise<Array<{name: string, displayName: string, open: boolean}>>} A promise that resolves to an array of billing account objects,
22
+ * each with 'name', 'displayName', and 'open' status. Returns an empty array on error.
23
+ */
24
+ import { getBillingClient } from '../clients.js';
25
+
17
26
  /**
18
27
  * Lists all accessible Google Cloud Billing Accounts.
19
28
  * @async
@@ -22,8 +31,7 @@ limitations under the License.
22
31
  * each with 'name', 'displayName', and 'open' status. Returns an empty array on error.
23
32
  */
24
33
  export async function listBillingAccounts() {
25
- const { CloudBillingClient } = await import('@google-cloud/billing');
26
- const client = new CloudBillingClient();
34
+ const client = await getBillingClient();
27
35
  try {
28
36
  const [accounts] = await client.listBillingAccounts();
29
37
  if (!accounts || accounts.length === 0) {
@@ -53,8 +61,7 @@ export async function attachProjectToBillingAccount(
53
61
  projectId,
54
62
  billingAccountName
55
63
  ) {
56
- const { CloudBillingClient } = await import('@google-cloud/billing');
57
- const client = new CloudBillingClient();
64
+ const client = await getBillingClient();
58
65
  const projectName = `projects/${projectId}`;
59
66
 
60
67
  if (!projectId) {
@@ -96,3 +103,82 @@ export async function attachProjectToBillingAccount(
96
103
  return null;
97
104
  }
98
105
  }
106
+
107
+ /**
108
+ * Checks if billing is enabled for a given project.
109
+ * @async
110
+ * @function isBillingEnabled
111
+ * @param {string} projectId - The ID of the project to check.
112
+ * @returns {Promise<boolean>} A promise that resolves to true if billing is enabled, false otherwise.
113
+ */
114
+ export async function isBillingEnabled(projectId) {
115
+ const client = await getBillingClient();
116
+ const projectName = `projects/${projectId}`;
117
+ try {
118
+ // getProjectBillingInfo requires cloudbilling.googleapis.com API to check billing status
119
+ // https://docs.cloud.google.com/billing/docs/reference/rest/v1/projects/getBillingInfo
120
+ const [billingInfo] = await client.getProjectBillingInfo({
121
+ name: projectName,
122
+ });
123
+ return billingInfo.billingEnabled;
124
+ } catch (error) {
125
+ console.error(
126
+ `Error checking billing status for project ${projectId}:`,
127
+ error.message || error
128
+ );
129
+ return false;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Ensures billing is enabled for a project. If not, attempts to enable it
135
+ * by attaching the single open billing account if one is found.
136
+ * @param {string} projectId The project ID to check billing for.
137
+ * @param {function} progressCallback A callback function for progress updates.
138
+ */
139
+ export async function ensureBillingEnabled(projectId, progressCallback) {
140
+ if (!(await isBillingEnabled(projectId))) {
141
+ // Billing is disabled, try to fix it.
142
+ const accounts = await listBillingAccounts();
143
+
144
+ if (accounts && accounts.length === 1 && accounts[0].open) {
145
+ // Exactly one open account found, try to attach it.
146
+ const account = accounts[0];
147
+ const attemptMessage = `Billing is not enabled for project ${projectId}. Found one open billing account: ${account.displayName} (${account.name}). Attempting to attach it...`;
148
+ console.log(attemptMessage);
149
+ if (progressCallback)
150
+ progressCallback({ level: 'info', data: attemptMessage });
151
+
152
+ const attachmentResult = await attachProjectToBillingAccount(
153
+ projectId,
154
+ account.name
155
+ );
156
+
157
+ if (!attachmentResult || !attachmentResult.billingEnabled) {
158
+ const attachFailMessage = `Failed to automatically attach project ${projectId} to billing account ${account.name}. Please enable billing manually: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`;
159
+ if (progressCallback)
160
+ progressCallback({ level: 'error', data: attachFailMessage });
161
+ throw new Error(attachFailMessage);
162
+ }
163
+ const attachSuccessMessage = `Successfully attached project ${projectId} to billing account ${account.name}.`;
164
+ console.log(attachSuccessMessage);
165
+ if (progressCallback)
166
+ progressCallback({ level: 'info', data: attachSuccessMessage });
167
+ // If we get here, billing is now enabled, and we can proceed to API checks.
168
+ } else {
169
+ // Cannot auto-attach. Throw error.
170
+ let reason;
171
+ if (!accounts || accounts.length === 0) {
172
+ reason = 'no billing accounts were found';
173
+ } else if (accounts.length > 1) {
174
+ reason = 'multiple billing accounts were found';
175
+ } else {
176
+ reason = `the only available billing account '${accounts[0].displayName}' is not open`;
177
+ }
178
+ const errorMessage = `Billing is not enabled for project ${projectId}, and it could not be enabled automatically because ${reason}. Please enable billing to use Google Cloud services: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`;
179
+ if (progressCallback)
180
+ progressCallback({ level: 'error', data: errorMessage });
181
+ throw new Error(errorMessage);
182
+ }
183
+ }
184
+ }
@@ -14,8 +14,15 @@ See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
16
 
17
+ import {
18
+ getCloudBuildClient,
19
+ getLoggingClient,
20
+ getRunClient,
21
+ getBuildsClient,
22
+ } from '../clients.js';
17
23
  import { callWithRetry } from './helpers.js';
18
24
  import { logAndProgress } from '../util/helpers.js';
25
+ import { checkCloudRunServiceExists } from './run.js';
19
26
 
20
27
  const DELAY_WAIT_FOR_BUILD_LOGS = 10000; // 10 seconds delay to allow logs to propagate
21
28
  const BUILD_LOGS_LINES_TO_FETCH = 100; // Number of log lines to fetch for build logs snippet
@@ -23,10 +30,10 @@ const BUILD_LOGS_LINES_TO_FETCH = 100; // Number of log lines to fetch for build
23
30
  /**
24
31
  * Triggers a Google Cloud Build job to build a container image from source code in a GCS bucket.
25
32
  * It uses either a Dockerfile found in the source or Google Cloud Buildpacks if no Dockerfile is present.
33
+ * It performs a dry-run deployment check before starting the build.
26
34
  * Waits for the build to complete and returns the build result.
27
35
  *
28
36
  * @async
29
- * @param {object} context - The context object containing clients and other parameters.
30
37
  * @param {string} projectId - The Google Cloud project ID.
31
38
  * @param {string} location - The Google Cloud region for the build.
32
39
  * @param {string} sourceBucketName - The GCS bucket name where the source code (zip) is stored.
@@ -39,7 +46,6 @@ const BUILD_LOGS_LINES_TO_FETCH = 100; // Number of log lines to fetch for build
39
46
  * @throws {Error} If the Cloud Build job fails, times out, or encounters an error during initiation or execution.
40
47
  */
41
48
  export async function triggerCloudBuild(
42
- context,
43
49
  projectId,
44
50
  location,
45
51
  sourceBucketName,
@@ -49,65 +55,107 @@ export async function triggerCloudBuild(
49
55
  hasDockerfile,
50
56
  progressCallback
51
57
  ) {
58
+ const serviceName = targetImageUrl.split('/').pop().split(/[:@]/)[0];
59
+ const parent = `projects/${projectId}/locations/${location}`;
60
+ const serviceFullName = `${parent}/services/${serviceName}`;
61
+ const buildsClient = await getBuildsClient(projectId);
62
+ const runClient = await getRunClient(projectId);
63
+ const serviceExists = await checkCloudRunServiceExists(
64
+ projectId,
65
+ location,
66
+ serviceName,
67
+ progressCallback
68
+ );
69
+ const cloudBuildClient = await getCloudBuildClient(projectId);
70
+ const loggingClient = await getLoggingClient(projectId);
52
71
  let buildSteps;
53
72
 
54
- if (hasDockerfile) {
55
- buildSteps = [
56
- {
57
- name: 'gcr.io/cloud-builders/docker',
58
- args: ['build', '-t', targetImageUrl, '.'],
59
- dir: '/workspace',
60
- },
61
- ];
62
- } else {
63
- buildSteps = [
64
- {
65
- name: 'gcr.io/k8s-skaffold/pack',
66
- entrypoint: 'pack',
67
- args: [
68
- 'build',
69
- targetImageUrl,
70
- '--builder',
71
- 'gcr.io/buildpacks/builder:latest',
72
- ],
73
- dir: '/workspace',
74
- },
75
- ];
73
+ const servicePatch = {
74
+ template: {
75
+ containers: [{ image: targetImageUrl }],
76
+ },
77
+ };
78
+
79
+ try {
80
+ if (serviceExists) {
81
+ await logAndProgress(
82
+ `Performing dry-run update for service ${serviceName}...`,
83
+ progressCallback
84
+ );
85
+ servicePatch.name = serviceFullName;
86
+ await callWithRetry(
87
+ () =>
88
+ runClient.updateService({
89
+ service: servicePatch,
90
+ validateOnly: true,
91
+ }),
92
+ 'run.updateService-dryrun'
93
+ );
94
+ } else {
95
+ await logAndProgress(
96
+ `Performing dry-run creation for service ${serviceName}...`,
97
+ progressCallback
98
+ );
99
+ await callWithRetry(
100
+ () =>
101
+ runClient.createService({
102
+ parent: parent,
103
+ service: servicePatch,
104
+ serviceId: serviceName,
105
+ validateOnly: true,
106
+ }),
107
+ 'run.createService-dryrun'
108
+ );
109
+ }
110
+ await logAndProgress(
111
+ `Dry-run validation successful.`,
112
+ progressCallback,
113
+ 'debug'
114
+ );
115
+ } catch (err) {
116
+ await logAndProgress(
117
+ `Dry-run validation failed for service ${serviceName}.`,
118
+ progressCallback,
119
+ 'error'
120
+ );
121
+ throw new Error(`Dry-run deployment failed: ${err.message}`);
76
122
  }
77
123
 
78
- const build = {
79
- source: {
80
- storageSource: {
81
- bucket: sourceBucketName,
82
- object: sourceBlobName,
83
- },
124
+ const request = {
125
+ parent: `projects/${projectId}/locations/${location}`,
126
+ storageSource: {
127
+ bucket: sourceBucketName,
128
+ object: sourceBlobName,
84
129
  },
85
- steps: buildSteps,
86
- images: [targetImageUrl],
130
+ imageUri: targetImageUrl,
131
+ client: 'cloud-run-mcp',
87
132
  };
133
+ if (hasDockerfile) {
134
+ request.dockerBuild = {};
135
+ } else {
136
+ request.buildpackBuild = {};
137
+ }
88
138
 
89
139
  await logAndProgress(
90
140
  `Initiating Cloud Build for gs://${sourceBucketName}/${sourceBlobName} in ${location}...`,
91
141
  progressCallback
92
142
  );
93
- const [operation] = await callWithRetry(
94
- () =>
95
- context.cloudBuildClient.createBuild({
96
- projectId: projectId,
97
- build: build,
98
- }),
99
- 'cloudBuild.createBuild'
100
- );
101
143
 
144
+ const [submitBuildResponse] = await callWithRetry(
145
+ () => buildsClient.submitBuild(request),
146
+ 'builds.submitBuild'
147
+ );
102
148
  await logAndProgress(`Cloud Build job started...`, progressCallback);
103
- const buildId = operation.metadata.build.id;
149
+ const encodedBuildId = submitBuildResponse.buildOperation.name
150
+ .split('/')
151
+ .pop();
152
+ const buildId = Buffer.from(encodedBuildId, 'base64').toString('ascii');
104
153
  let completedBuild;
105
154
  while (true) {
106
155
  const [getBuildOperation] = await callWithRetry(
107
156
  () =>
108
- context.cloudBuildClient.getBuild({
109
- projectId: projectId,
110
- id: buildId,
157
+ cloudBuildClient.getBuild({
158
+ name: `projects/${projectId}/locations/${location}/builds/${buildId}`,
111
159
  }),
112
160
  `cloudBuild.getBuild ${buildId}`
113
161
  );
@@ -132,10 +180,9 @@ export async function triggerCloudBuild(
132
180
  `Cloud Build job ${buildId} completed successfully.`,
133
181
  progressCallback
134
182
  );
135
- await logAndProgress(
136
- `Image built: ${completedBuild.results.images[0].name}`,
137
- progressCallback
138
- );
183
+ const builtImage = completedBuild.results.images[0].name;
184
+ await logAndProgress(`Image built: ${builtImage}`, progressCallback);
185
+
139
186
  return completedBuild;
140
187
  } else {
141
188
  const failureMessage = `Cloud Build job ${buildId} failed with status: ${completedBuild.status}`;
@@ -160,7 +207,7 @@ export async function triggerCloudBuild(
160
207
  // Fetch the most recent N log entries
161
208
  const [entries] = await callWithRetry(
162
209
  () =>
163
- context.loggingClient.getEntries({
210
+ loggingClient.getEntries({
164
211
  filter: logFilter,
165
212
  orderBy: 'timestamp desc', // Get latest logs first
166
213
  pageSize: BUILD_LOGS_LINES_TO_FETCH,
@@ -14,6 +14,14 @@ See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
16
 
17
+ import { getServiceUsageClient } from '../clients.js';
18
+ import { ensureBillingEnabled } from './billing.js';
19
+
20
+ const PREREQUISITE_APIS = [
21
+ 'serviceusage.googleapis.com',
22
+ 'cloudbilling.googleapis.com',
23
+ ];
24
+
17
25
  /**
18
26
  * Calls a function with retry logic for GCP API calls.
19
27
  * Retries on gRPC error code 7 (PERMISSION_DENIED).
@@ -84,13 +92,55 @@ async function checkAndEnableApi(
84
92
  }
85
93
  }
86
94
 
95
+ /**
96
+ * Enables a single Google Cloud API with retry logic.
97
+ * @param {string} projectId - The Google Cloud project ID.
98
+ * @param {string} api - The API identifier (e.g., 'run.googleapis.com').
99
+ * @param {function(string, string=): void} progressCallback - A function to call with progress updates.
100
+ * @returns {Promise<void>} A promise that resolves when the API is enabled.
101
+ * @throws {Error} If the API fails to enable or if there's an issue checking its status.
102
+ */
103
+ export async function enableApiWithRetry(projectId, api, progressCallback) {
104
+ const serviceUsageClient = await getServiceUsageClient(projectId);
105
+ const serviceName = `projects/${projectId}/services/${api}`;
106
+ try {
107
+ await checkAndEnableApi(
108
+ serviceUsageClient,
109
+ serviceName,
110
+ api,
111
+ progressCallback
112
+ );
113
+ } catch (error) {
114
+ // First attempt failed, log a warning and retry once after a delay.
115
+ const warnMsg = `Failed to check/enable ${api}, retrying in 1s...`;
116
+ console.warn(warnMsg);
117
+ if (progressCallback) progressCallback({ level: 'warn', data: warnMsg });
118
+
119
+ await new Promise((resolve) => setTimeout(resolve, 1000));
120
+ try {
121
+ await checkAndEnableApi(
122
+ serviceUsageClient,
123
+ serviceName,
124
+ api,
125
+ progressCallback
126
+ );
127
+ } catch (retryError) {
128
+ // If the retry also fails, throw an error.
129
+ const errorMessage = `Failed to ensure API [${api}] is enabled after retry. Please check manually.`;
130
+ console.error(errorMessage, retryError);
131
+ if (progressCallback)
132
+ progressCallback({ level: 'error', data: errorMessage });
133
+ throw new Error(errorMessage);
134
+ }
135
+ }
136
+ }
137
+
87
138
  /**
88
139
  * Ensures that the specified Google Cloud APIs are enabled for the given project.
89
140
  * If an API is not enabled, it attempts to enable it. Retries any failure once after 1s.
90
141
  * Throws an error if an API cannot be enabled.
91
142
  *
92
143
  * @async
93
- * @param {object} context - The context object containing clients and other parameters.
94
144
  * @param {string} projectId - The Google Cloud project ID.
95
145
  * @param {string[]} apis - An array of API identifiers to check and enable (e.g., 'run.googleapis.com').
96
146
  * @param {function(string, string=): void} progressCallback - A function to call with progress updates.
@@ -98,48 +148,20 @@ async function checkAndEnableApi(
98
148
  * @throws {Error} If an API fails to enable or if there's an issue checking its status.
99
149
  * @returns {Promise<void>} A promise that resolves when all specified APIs are enabled.
100
150
  */
101
- export async function ensureApisEnabled(
102
- context,
103
- projectId,
104
- apis,
105
- progressCallback
106
- ) {
151
+ export async function ensureApisEnabled(projectId, apis, progressCallback) {
152
+ for (const api of PREREQUISITE_APIS) {
153
+ await enableApiWithRetry(projectId, api, progressCallback);
154
+ }
155
+
156
+ // Before enabling other APIs, ensure billing is enabled.
157
+ await ensureBillingEnabled(projectId, progressCallback);
158
+
107
159
  const message = 'Checking and enabling required APIs...';
108
160
  console.log(message);
109
161
  if (progressCallback) progressCallback({ level: 'info', data: message });
110
162
 
111
163
  for (const api of apis) {
112
- const serviceName = `projects/${projectId}/services/${api}`;
113
- try {
114
- await checkAndEnableApi(
115
- context.serviceUsageClient,
116
- serviceName,
117
- api,
118
- progressCallback
119
- );
120
- } catch (error) {
121
- // First attempt failed, log a warning and retry once after a delay.
122
- const warnMsg = `Failed to check/enable ${api}, retrying in 1s...`;
123
- console.warn(warnMsg);
124
- if (progressCallback) progressCallback({ level: 'warn', data: warnMsg });
125
-
126
- await new Promise((resolve) => setTimeout(resolve, 1000));
127
- try {
128
- await checkAndEnableApi(
129
- context.serviceUsageClient,
130
- serviceName,
131
- api,
132
- progressCallback
133
- );
134
- } catch (retryError) {
135
- // If the retry also fails, throw an error.
136
- const errorMessage = `Failed to ensure API [${api}] is enabled after retry. Please check manually.`;
137
- console.error(errorMessage, retryError);
138
- if (progressCallback)
139
- progressCallback({ level: 'error', data: errorMessage });
140
- throw new Error(errorMessage);
141
- }
142
- }
164
+ await enableApiWithRetry(projectId, api, progressCallback);
143
165
  }
144
166
  const successMsg = 'All required APIs are enabled.';
145
167
  console.log(successMsg);