@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
@@ -37,12 +37,16 @@ describe('registerTools', () => {
37
37
  registerTool: mock.fn(),
38
38
  };
39
39
 
40
- const { registerTools } = await esmock('../../tools/tools.js', {}, {
41
- '../../lib/gcp-projects.js': {
42
- listProjects: () =>
43
- Promise.resolve([{ id: 'project1' }, { id: 'project2' }]),
44
- },
45
- });
40
+ const { registerTools } = await esmock(
41
+ '../../tools/tools.js',
42
+ {},
43
+ {
44
+ '../../lib/cloud-api/projects.js': {
45
+ listProjects: () =>
46
+ Promise.resolve([{ id: 'project1' }, { id: 'project2' }]),
47
+ },
48
+ }
49
+ );
46
50
 
47
51
  registerTools(server, { gcpCredentialsAvailable: true });
48
52
 
@@ -68,15 +72,19 @@ describe('registerTools', () => {
68
72
  registerTool: mock.fn(),
69
73
  };
70
74
 
71
- const { registerTools } = await esmock('../../tools/tools.js', {}, {
72
- '../../lib/gcp-projects.js': {
73
- createProjectAndAttachBilling: (projectId) =>
74
- Promise.resolve({
75
- projectId: projectId,
76
- billingMessage: 'billing message',
77
- }),
78
- },
79
- });
75
+ const { registerTools } = await esmock(
76
+ '../../tools/tools.js',
77
+ {},
78
+ {
79
+ '../../lib/cloud-api/projects.js': {
80
+ createProjectAndAttachBilling: (projectId) =>
81
+ Promise.resolve({
82
+ projectId: projectId,
83
+ billingMessage: `Project ${projectId} created successfully. Billing attached.`,
84
+ }),
85
+ },
86
+ }
87
+ );
80
88
 
81
89
  registerTools(server, { gcpCredentialsAvailable: true });
82
90
 
@@ -89,7 +97,7 @@ describe('registerTools', () => {
89
97
  content: [
90
98
  {
91
99
  type: 'text',
92
- text: 'Successfully created GCP project with ID "my-project". You can now use this project ID for deployments.',
100
+ text: 'Project my-project created successfully. Billing attached.',
93
101
  },
94
102
  ],
95
103
  });
@@ -100,15 +108,20 @@ describe('registerTools', () => {
100
108
  registerTool: mock.fn(),
101
109
  };
102
110
 
103
- const { registerTools } = await esmock('../../tools/tools.js', {}, {
104
- '../../lib/gcp-projects.js': {
105
- createProjectAndAttachBilling: () =>
106
- Promise.resolve({
107
- projectId: 'generated-project',
108
- billingMessage: 'billing message',
109
- }),
110
- },
111
- });
111
+ const { registerTools } = await esmock(
112
+ '../../tools/tools.js',
113
+ {},
114
+ {
115
+ '../../lib/cloud-api/projects.js': {
116
+ createProjectAndAttachBilling: () =>
117
+ Promise.resolve({
118
+ projectId: 'generated-project',
119
+ billingMessage:
120
+ 'Project generated-project created successfully. Billing attached.',
121
+ }),
122
+ },
123
+ }
124
+ );
112
125
 
113
126
  registerTools(server, { gcpCredentialsAvailable: true });
114
127
 
@@ -121,7 +134,7 @@ describe('registerTools', () => {
121
134
  content: [
122
135
  {
123
136
  type: 'text',
124
- text: 'Successfully created GCP project with ID "generated-project". You can now use this project ID for deployments.',
137
+ text: 'Project generated-project created successfully. Billing attached.',
125
138
  },
126
139
  ],
127
140
  });
@@ -134,15 +147,25 @@ describe('registerTools', () => {
134
147
  registerTool: mock.fn(),
135
148
  };
136
149
 
137
- const { registerTools } = await esmock('../../tools/tools.js', {}, {
138
- '../../lib/cloud-run-services.js': {
139
- listServices: () =>
140
- Promise.resolve([
141
- { name: 'service1', uri: 'uri1' },
142
- { name: 'service2', uri: 'uri2' },
143
- ]),
144
- },
145
- });
150
+ const { registerTools } = await esmock(
151
+ '../../tools/tools.js',
152
+ {},
153
+ {
154
+ '../../lib/cloud-api/run.js': {
155
+ listServices: () =>
156
+ Promise.resolve({
157
+ 'my-region1': [
158
+ { name: 'service1', uri: 'uri1' },
159
+ { name: 'service2', uri: 'uri2' },
160
+ ],
161
+ 'my-region2': [
162
+ { name: 'service3', uri: 'uri3' },
163
+ { name: 'service4', uri: 'uri4' },
164
+ ],
165
+ }),
166
+ },
167
+ }
168
+ );
146
169
 
147
170
  registerTools(server, { gcpCredentialsAvailable: true });
148
171
 
@@ -151,14 +174,17 @@ describe('registerTools', () => {
151
174
  ).arguments[2];
152
175
  const result = await handler({
153
176
  project: 'my-project',
154
- region: 'my-region',
155
177
  });
156
178
 
157
179
  assert.deepStrictEqual(result, {
158
180
  content: [
159
181
  {
160
182
  type: 'text',
161
- text: 'Services in project my-project (location my-region):\n- service1 (URL: uri1)\n- service2 (URL: uri2)',
183
+ text: 'Services in project my-project (location my-region1):\n- service1 (URL: uri1)\n- service2 (URL: uri2)',
184
+ },
185
+ {
186
+ type: 'text',
187
+ text: 'Services in project my-project (location my-region2):\n- service3 (URL: uri3)\n- service4 (URL: uri4)',
162
188
  },
163
189
  ],
164
190
  });
@@ -171,16 +197,20 @@ describe('registerTools', () => {
171
197
  registerTool: mock.fn(),
172
198
  };
173
199
 
174
- const { registerTools } = await esmock('../../tools/tools.js', {}, {
175
- '../../lib/cloud-run-services.js': {
176
- getService: () =>
177
- Promise.resolve({
178
- name: 'my-service',
179
- uri: 'my-uri',
180
- lastModifier: 'me',
181
- }),
182
- },
183
- });
200
+ const { registerTools } = await esmock(
201
+ '../../tools/tools.js',
202
+ {},
203
+ {
204
+ '../../lib/cloud-api/run.js': {
205
+ getService: () =>
206
+ Promise.resolve({
207
+ name: 'my-service',
208
+ uri: 'my-uri',
209
+ lastModifier: 'me',
210
+ }),
211
+ },
212
+ }
213
+ );
184
214
 
185
215
  registerTools(server, { gcpCredentialsAvailable: true });
186
216
 
@@ -222,11 +252,15 @@ describe('registerTools', () => {
222
252
  return Promise.resolve({ logs: 'log3\nlog4', requestOptions: null });
223
253
  };
224
254
 
225
- const { registerTools } = await esmock('../../tools/tools.js', {}, {
226
- '../../lib/cloud-run-services.js': {
227
- getServiceLogs: getServiceLogs,
228
- },
229
- });
255
+ const { registerTools } = await esmock(
256
+ '../../tools/tools.js',
257
+ {},
258
+ {
259
+ '../../lib/cloud-api/run.js': {
260
+ getServiceLogs: getServiceLogs,
261
+ },
262
+ }
263
+ );
230
264
 
231
265
  registerTools(server, { gcpCredentialsAvailable: true });
232
266
 
@@ -250,19 +284,21 @@ describe('registerTools', () => {
250
284
  });
251
285
  });
252
286
 
253
-
254
-
255
287
  describe('deploy_local_folder', () => {
256
288
  it('should deploy local folder', async () => {
257
289
  const server = {
258
290
  registerTool: mock.fn(),
259
291
  };
260
292
 
261
- const { registerTools } = await esmock('../../tools/tools.js', {}, {
262
- '../../lib/cloud-run-deploy.js': {
263
- deploy: () => Promise.resolve({ uri: 'my-uri' }),
264
- },
265
- });
293
+ const { registerTools } = await esmock(
294
+ '../../tools/tools.js',
295
+ {},
296
+ {
297
+ '../../lib/deployment/deployer.js': {
298
+ deploy: () => Promise.resolve({ uri: 'my-uri' }),
299
+ },
300
+ }
301
+ );
266
302
 
267
303
  registerTools(server, { gcpCredentialsAvailable: true });
268
304
 
@@ -296,11 +332,15 @@ describe('registerTools', () => {
296
332
  registerTool: mock.fn(),
297
333
  };
298
334
 
299
- const { registerTools } = await esmock('../../tools/tools.js', {}, {
300
- '../../lib/cloud-run-deploy.js': {
301
- deploy: () => Promise.resolve({ uri: 'my-uri' }),
302
- },
303
- });
335
+ const { registerTools } = await esmock(
336
+ '../../tools/tools.js',
337
+ {},
338
+ {
339
+ '../../lib/deployment/deployer.js': {
340
+ deploy: () => Promise.resolve({ uri: 'my-uri' }),
341
+ },
342
+ }
343
+ );
304
344
 
305
345
  registerTools(server, { gcpCredentialsAvailable: true });
306
346
 
@@ -334,11 +374,15 @@ describe('registerTools', () => {
334
374
  registerTool: mock.fn(),
335
375
  };
336
376
 
337
- const { registerTools } = await esmock('../../tools/tools.js', {}, {
338
- '../../lib/cloud-run-deploy.js': {
339
- deployImage: () => Promise.resolve({ uri: 'my-uri' }),
340
- },
341
- });
377
+ const { registerTools } = await esmock(
378
+ '../../tools/tools.js',
379
+ {},
380
+ {
381
+ '../../lib/deployment/deployer.js': {
382
+ deployImage: () => Promise.resolve({ uri: 'my-uri' }),
383
+ },
384
+ }
385
+ );
342
386
 
343
387
  registerTools(server, { gcpCredentialsAvailable: true });
344
388
 
@@ -372,7 +416,7 @@ describe('registerTools', () => {
372
416
  registerTool: mock.fn(),
373
417
  };
374
418
 
375
- const { registerTools } = await esmock('../../tools/tools.js', {});
419
+ const { registerTools } = await esmock('../../tools/tools.js', {});
376
420
 
377
421
  registerTools(server, { gcpCredentialsAvailable: false });
378
422
 
@@ -15,66 +15,68 @@ limitations under the License.
15
15
  */
16
16
 
17
17
  import { test } from 'node:test';
18
+ import { getServiceLogs, listServices } from '../../lib/cloud-api/run.js';
18
19
  import assert from 'node:assert';
19
- import {
20
- getServiceLogs,
21
- listServices,
22
- } from '../../lib/cloud-run-services.js';
23
20
 
24
21
  /**
25
- * Gets service details from GOOGLE_CLOUD_PROJECT or command line arguments.
26
- * @returns {{projectId: string, region: string, serviceId: string}} The service details
22
+ * Get the GCP project ID from GOOGLE_CLOUD_PROJECT or command line argument.
23
+ * @returns {string} The GCP project ID
27
24
  */
28
- function getServiceDetails() {
25
+ function getProjectId() {
29
26
  const projectId = process.env.GOOGLE_CLOUD_PROJECT || process.argv[2];
30
- const [, , arg1, arg2, arg3] = process.argv;
31
-
32
- let region = 'europe-west1';
33
- let serviceId;
34
-
35
- if (process.env.GOOGLE_CLOUD_PROJECT) {
36
- region = arg1 || 'europe-west1';
37
- serviceId = arg2;
38
- } else {
39
- region = arg2 || 'europe-west1';
40
- serviceId = arg3;
41
- }
42
-
43
27
  if (!projectId) {
44
28
  console.error(
45
- 'Usage: node <script> <projectId> [region] [serviceId] or set GOOGLE_CLOUD_PROJECT'
29
+ 'Usage: node <script> <projectId> or set GOOGLE_CLOUD_PROJECT'
46
30
  );
47
31
  process.exit(1);
48
32
  }
33
+ console.log(`Using Project ID: ${projectId}`);
34
+ return projectId;
35
+ }
49
36
 
50
- console.log(`Using:
51
- Project ID: ${projectId}
52
- Region: ${region}`);
53
- if (serviceId) {
54
- console.log(`Service ID: ${serviceId}`);
55
- }
37
+ /**
38
+ * Gets service details from GOOGLE_CLOUD_PROJECT or command line arguments.
39
+ * @returns {{projectId: string, region: string, serviceId: string}} The service details
40
+ */
41
+ async function getServiceDetails() {
42
+ const projectId = getProjectId();
43
+ let region, serviceId;
56
44
 
57
- return { projectId, region, serviceId };
58
- }
45
+ try {
46
+ const allServices = await listServices(projectId);
47
+
48
+ if (!allServices || Object.keys(allServices).length === 0) {
49
+ assert.fail('No services found for the given project.');
50
+ }
51
+
52
+ const regions = Object.keys(allServices);
53
+ region = regions[0];
54
+
55
+ if (!allServices[region] || allServices[region].length === 0) {
56
+ assert.fail(`No services found in region: ${region}`);
57
+ }
58
+
59
+ const serviceName = allServices[region][0].name;
60
+ serviceId = serviceName.split('/').pop();
61
+ console.log(`Using region - ${region} and service ID - ${serviceId}`);
59
62
 
60
- const { projectId, region, serviceId } = getServiceDetails();
63
+ return { projectId, region, serviceId };
64
+ } catch (error) {
65
+ // Better error handling for the API call itself
66
+ console.error('Error fetching services:', error.message);
67
+ throw error; // Re-throw the error to fail the test
68
+ }
69
+ }
61
70
 
62
71
  test('should list services', async () => {
63
- const services = await listServices(projectId, region);
64
- assert(Array.isArray(services), 'services should be an array');
65
- console.log('Services found:', services.length);
72
+ const projectId = getProjectId();
73
+ const services = await listServices(projectId);
74
+ console.log('Services found:', services ? Object.keys(services) : 'None');
75
+ console.log('All Services', services);
66
76
  });
67
77
 
68
78
  test('should fetch service logs', async () => {
69
- if (!serviceId) {
70
- console.log('Skipping service log test: no serviceId provided.');
71
- return;
72
- }
73
-
74
- console.log(
75
- `
76
- Fetching logs for service "${serviceId}" in project "${projectId}" (region: ${region})...`
77
- );
79
+ const { projectId, region, serviceId } = await getServiceDetails();
78
80
 
79
81
  const result = await getServiceLogs(projectId, region, serviceId);
80
82
 
@@ -85,5 +87,3 @@ Fetching logs for service "${serviceId}" in project "${projectId}" (region: ${re
85
87
  console.log('No logs found for this service.');
86
88
  }
87
89
  });
88
-
89
-
@@ -16,68 +16,15 @@ limitations under the License.
16
16
 
17
17
  import { describe, test } from 'node:test';
18
18
  import assert from 'node:assert';
19
- import { deploy, deployImage } from '../../lib/cloud-run-deploy.js';
20
- import fs from 'fs/promises';
21
- import path from 'path';
19
+ import { deploy, deployImage } from '../../lib/deployment/deployer.js';
22
20
 
23
21
  const projectId = process.env.GOOGLE_CLOUD_PROJECT || process.argv[2];
24
22
  if (!projectId) {
25
- console.error(
26
- 'Usage: node <script> <projectId> or set GOOGLE_CLOUD_PROJECT'
27
- );
23
+ console.error('Usage: node <script> <projectId> or set GOOGLE_CLOUD_PROJECT');
28
24
  process.exit(1);
29
25
  }
30
26
 
31
27
  describe('Cloud Run Deployments', () => {
32
- test('should deploy a container image', async () => {
33
- const configImageDeploy = {
34
- projectId: projectId,
35
- serviceName: 'hello-from-image',
36
- region: 'europe-west1',
37
- imageUrl: 'gcr.io/cloudrun/hello',
38
- };
39
- await deployImage(configImageDeploy);
40
- });
41
-
42
- test('should fail to deploy with invalid files', async () => {
43
- const configFailingBuild = {
44
- projectId: projectId,
45
- serviceName: 'example-failing-app',
46
- region: 'europe-west1',
47
- files: [
48
- {
49
- filename: 'main.txt',
50
- content:
51
- 'This is not a valid application source file and should cause a build failure.',
52
- },
53
- ],
54
- };
55
- await assert.rejects(deploy(configFailingBuild));
56
- });
57
-
58
-
59
-
60
- test('should deploy a Go app with file content (Buildpacks)', async () => {
61
- const mainGoContent = await fs.readFile(
62
- path.resolve('example-sources-to-deploy/main.go'),
63
- 'utf-8'
64
- );
65
- const goModContent = await fs.readFile(
66
- path.resolve('example-sources-to-deploy/go.mod'),
67
- 'utf-8'
68
- );
69
- const configGoWithContent = {
70
- projectId: projectId,
71
- serviceName: 'example-go-app-content',
72
- region: 'europe-west1',
73
- files: [
74
- { filename: 'main.go', content: mainGoContent },
75
- { filename: 'go.mod', content: goModContent },
76
- ],
77
- };
78
- await deploy(configGoWithContent);
79
- });
80
-
81
28
  test('should fail to deploy without a service name', async () => {
82
29
  const config = {
83
30
  projectId: projectId,
@@ -121,4 +68,4 @@ describe('Cloud Run Deployments', () => {
121
68
  message: 'Error: projectId is required in the configuration object.',
122
69
  });
123
70
  });
124
- });
71
+ });
@@ -17,7 +17,7 @@ limitations under the License.
17
17
  import assert from 'node:assert/strict';
18
18
  import { describe, it, mock, beforeEach, afterEach } from 'node:test';
19
19
  import { GoogleAuth } from 'google-auth-library';
20
- import { ensureGCPCredentials } from '../../lib/gcp-auth-check.js';
20
+ import { ensureGCPCredentials } from '../../lib/cloud-api/auth.js';
21
21
 
22
22
  describe('ensureGCPCredentials', () => {
23
23
  let originalConsoleLog;
@@ -15,21 +15,9 @@ limitations under the License.
15
15
  */
16
16
 
17
17
  import { test } from 'node:test';
18
- import assert from 'node:assert';
19
- import { createProjectAndAttachBilling } from '../../lib/gcp-projects.js';
18
+ import { setupProject } from './test-helpers.js';
20
19
 
21
- test('should create a new project and attach billing', async () => {
20
+ test('should create a new project and attach billing', async (t) => {
22
21
  console.log('Attempting to create a new project and attach billing...');
23
- const newProjectResult = await createProjectAndAttachBilling();
24
-
25
- assert(newProjectResult, 'newProjectResult should not be null');
26
- assert(
27
- newProjectResult.projectId,
28
- 'newProjectResult.projectId should not be null'
29
- );
30
-
31
- console.log(
32
- `Successfully created project: ${newProjectResult.projectId}`
33
- );
34
- console.log(newProjectResult.billingMessage);
22
+ await setupProject(t);
35
23
  });
@@ -0,0 +1,175 @@
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
+ import assert from 'node:assert';
17
+ import {
18
+ createProjectAndAttachBilling,
19
+ deleteProject,
20
+ generateProjectId,
21
+ } from '../../lib/cloud-api/projects.js';
22
+ import {
23
+ callWithRetry,
24
+ ensureApisEnabled,
25
+ } from '../../lib/cloud-api/helpers.js';
26
+
27
+ /**
28
+ * Gets project number from project ID.
29
+ * @param {string} projectId
30
+ * @returns {Promise<string>} project number
31
+ */
32
+ export async function getProjectNumber(projectId) {
33
+ const { ProjectsClient } = await import('@google-cloud/resource-manager');
34
+ const client = new ProjectsClient();
35
+ try {
36
+ const [project] = await client.getProject({
37
+ name: `projects/${projectId}`,
38
+ });
39
+ // project.name is in format projects/123456
40
+ return project.name.split('/')[1];
41
+ } catch (error) {
42
+ console.error(
43
+ `Error getting project number for project ${projectId}:`,
44
+ error.message
45
+ );
46
+ throw error;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Adds an IAM policy binding to a project.
52
+ * @param {string} projectId The project ID.
53
+ * @param {string} member The member to add, e.g., 'user:foo@example.com'.
54
+ * @param {string} role The role to grant, e.g., 'roles/viewer'.
55
+ */
56
+ export async function addIamPolicyBinding(projectId, member, role) {
57
+ const { ProjectsClient } = await import('@google-cloud/resource-manager');
58
+ const client = new ProjectsClient();
59
+
60
+ console.log(
61
+ `Adding IAM binding for ${member} with role ${role} to project ${projectId}`
62
+ );
63
+
64
+ try {
65
+ const [policy] = await client.getIamPolicy({
66
+ resource: `projects/${projectId}`,
67
+ });
68
+
69
+ console.log('Current IAM Policy:', JSON.stringify(policy, null, 2));
70
+
71
+ // Check if the binding already exists
72
+ const binding = policy.bindings.find((b) => b.role === role);
73
+ if (binding) {
74
+ if (!binding.members.includes(member)) {
75
+ binding.members.push(member);
76
+ }
77
+ } else {
78
+ policy.bindings.push({
79
+ role: role,
80
+ members: [member],
81
+ });
82
+ }
83
+
84
+ console.log('Updated IAM Policy:', JSON.stringify(policy, null, 2));
85
+
86
+ // Set the updated policy
87
+ await client.setIamPolicy({
88
+ resource: `projects/${projectId}`,
89
+ policy: policy,
90
+ });
91
+
92
+ console.log(
93
+ `Successfully added IAM binding for ${member} with role ${
94
+ role
95
+ } to project ${projectId}`
96
+ );
97
+ } catch (error) {
98
+ console.error(
99
+ `Error adding IAM policy binding to project ${projectId}:`,
100
+ error.message
101
+ );
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Create project, attach billing.
108
+ * @returns {Promise<string>} projectId
109
+ */
110
+ export async function setupProject() {
111
+ const projectId = 'test-' + generateProjectId();
112
+ console.log(`Generated project ID: ${projectId}`);
113
+ const parent = process.env.GCP_PARENT || process.argv[2];
114
+ const newProjectResult = await createProjectAndAttachBilling(
115
+ projectId,
116
+ parent
117
+ );
118
+ assert(newProjectResult, 'newProjectResult should not be null');
119
+ assert(
120
+ newProjectResult.projectId,
121
+ 'newProjectResult.projectId should not be null'
122
+ );
123
+ assert(
124
+ newProjectResult.billingMessage,
125
+ 'newProjectResult.billingMessage should not be null'
126
+ );
127
+ assert(
128
+ newProjectResult.billingMessage.startsWith(
129
+ `Project ${newProjectResult.projectId} created successfully.`
130
+ ),
131
+ 'newProjectResult.billingMessage should start with success message'
132
+ );
133
+ console.log(`Successfully created project: ${newProjectResult.projectId}`);
134
+ console.log(newProjectResult.billingMessage);
135
+
136
+ return projectId;
137
+ }
138
+
139
+ /**
140
+ * Delete project
141
+ * @param {string} projectId
142
+ */
143
+ export async function cleanupProject(projectId) {
144
+ try {
145
+ await deleteProject(projectId);
146
+ console.log(`Successfully deleted project: ${projectId}`);
147
+ } catch (e) {
148
+ console.error(`Failed to delete project ${projectId}:`, e.message);
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Enable APIs and set IAM permissions for source deployments.
154
+ * Note: This function is only needed for Cloud Build since it uses the compute
155
+ * default service account. The compute service account needs the editor role to
156
+ * deploy to Cloud Run, which is usually granted by default, but in this case we
157
+ * ensure it due to restrictions in some organizations.
158
+ * @param {string} projectId
159
+ */
160
+ export async function setSourceDeployProjectPermissions(projectId) {
161
+ const { ServiceUsageClient } = await import('@google-cloud/service-usage');
162
+ const serviceUsageClient = new ServiceUsageClient({ projectId });
163
+ const context = {
164
+ serviceUsageClient: serviceUsageClient,
165
+ };
166
+ await ensureApisEnabled(context, projectId, ['run.googleapis.com']);
167
+ console.log('Adding editor role to Compute SA...');
168
+ const projectNumber = await getProjectNumber(projectId);
169
+ const member = `serviceAccount:${projectNumber}-compute@developer.gserviceaccount.com`;
170
+ await callWithRetry(
171
+ () => addIamPolicyBinding(projectId, member, 'roles/editor'),
172
+ `addIamPolicyBinding roles/editor to ${member}`
173
+ );
174
+ console.log('Compute SA editor role added.');
175
+ }