@google-cloud/cloud-run-mcp 1.6.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.
@@ -24,27 +24,23 @@ import { ensureArtifactRegistryRepoExists } from '../cloud-api/registry.js';
24
24
  import { triggerCloudBuild } from '../cloud-api/build.js';
25
25
  import { logAndProgress } from '../util/helpers.js';
26
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';
27
37
 
28
- // Configuration
29
- const REPO_NAME = 'mcp-cloud-run-deployments';
30
- const ZIP_FILE_NAME = 'source.zip';
31
- const IMAGE_TAG = 'latest';
32
-
33
- // APIs required for deploying from source code.
34
- const REQUIRED_APIS_FOR_SOURCE_DEPLOY = [
35
- 'iam.googleapis.com',
36
- 'storage.googleapis.com',
37
- 'cloudbuild.googleapis.com',
38
- 'artifactregistry.googleapis.com',
39
- 'run.googleapis.com',
40
- 'serviceusage.googleapis.com',
41
- ];
42
-
43
- // APIs required for deploying a container image.
44
- const REQUIRED_APIS_FOR_IMAGE_DEPLOY = [
45
- 'run.googleapis.com',
46
- 'serviceusage.googleapis.com',
47
- ];
38
+ import {
39
+ DEPLOYMENT_CONFIG,
40
+ DEPLOYMENT_TYPES,
41
+ MAX_ALLOWED_DIRECT_SOURCE_SIZE_BYTES,
42
+ REQUIRED_APIS,
43
+ } from './constants.js';
48
44
 
49
45
  /**
50
46
  * Deploys or updates a Cloud Run service with the specified container image.
@@ -68,7 +64,10 @@ async function deployToCloudRun(
68
64
  serviceId,
69
65
  imgUrl,
70
66
  progressCallback,
71
- skipIamCheck
67
+ skipIamCheck,
68
+ deploymentType,
69
+ zippedSourceContainer,
70
+ runtime
72
71
  ) {
73
72
  const parent = context.runClient.locationPath(projectId, location);
74
73
  const servicePath = context.runClient.servicePath(
@@ -78,13 +77,20 @@ async function deployToCloudRun(
78
77
  );
79
78
  const revisionName = `${serviceId}-${Date.now()}`; // Generate a unique revision name
80
79
 
80
+ const containers =
81
+ deploymentType === DEPLOYMENT_TYPES.NO_BUILD
82
+ ? [zippedSourceContainer]
83
+ : [{ image: imgUrl }];
84
+
81
85
  const service = {
82
86
  template: {
83
87
  revision: revisionName,
84
- containers: [{ image: imgUrl }],
88
+ containers,
85
89
  },
86
90
  labels: {
87
- 'created-by': 'cloud-run-mcp',
91
+ 'created-by': DEPLOYMENT_CONFIG.LABEL_CREATED_BY,
92
+ 'deployment-type': deploymentType,
93
+ runtime: runtime,
88
94
  },
89
95
  };
90
96
 
@@ -213,6 +219,195 @@ async function deployToCloudRun(
213
219
  }
214
220
  }
215
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
+
216
411
  /**
217
412
  * Deploys a service to Google Cloud Run.
218
413
  * @param {object} config - The deployment configuration.
@@ -257,8 +452,6 @@ export async function deploy({
257
452
  }
258
453
  }
259
454
 
260
- const path = await import('path');
261
- const fs = await import('fs');
262
455
  const { Storage } = await import('@google-cloud/storage');
263
456
  const { CloudBuildClient } = await import('@google-cloud/cloudbuild');
264
457
  const { ArtifactRegistryClient } = await import(
@@ -282,103 +475,56 @@ export async function deploy({
282
475
  await ensureApisEnabled(
283
476
  context,
284
477
  projectId,
285
- REQUIRED_APIS_FOR_SOURCE_DEPLOY,
478
+ REQUIRED_APIS.SOURCE_DEPLOY,
286
479
  progressCallback
287
480
  );
288
481
 
289
482
  const bucketName = `${projectId}-source-bucket`;
290
- const imageUrl = `${region}-docker.pkg.dev/${projectId}/${REPO_NAME}/${serviceName}:${IMAGE_TAG}`;
291
483
 
292
484
  await logAndProgress(`Project: ${projectId}`, progressCallback);
293
485
  await logAndProgress(`Region: ${region}`, progressCallback);
294
486
  await logAndProgress(`Service Name: ${serviceName}`, progressCallback);
295
487
  await logAndProgress(`Files to deploy: ${files.length}`, progressCallback);
296
488
 
297
- let hasDockerfile = false;
298
- if (
299
- files.length === 1 &&
300
- typeof files[0] === 'string' &&
301
- fs.statSync(files[0]).isDirectory()
302
- ) {
303
- // Handle folder deployment: check for Dockerfile inside the folder
304
- const dockerfilePath = path.join(files[0], 'Dockerfile');
305
- const dockerfilePathLowerCase = path.join(files[0], 'dockerfile');
306
- if (
307
- fs.existsSync(dockerfilePath) ||
308
- fs.existsSync(dockerfilePathLowerCase)
309
- ) {
310
- hasDockerfile = true;
311
- }
312
- } else {
313
- // Handle file list deployment or file content deployment
314
- for (const file of files) {
315
- if (typeof file === 'string') {
316
- if (path.basename(file).toLowerCase() === 'dockerfile') {
317
- hasDockerfile = true;
318
- break;
319
- }
320
- } else if (typeof file === 'object' && file.filename) {
321
- if (path.basename(file.filename).toLowerCase() === 'dockerfile') {
322
- hasDockerfile = true;
323
- break;
324
- }
325
- }
326
- }
327
- }
328
- await logAndProgress(`Dockerfile: ${hasDockerfile}`, progressCallback);
329
-
330
- const bucket = await ensureStorageBucketExists(
331
- context,
332
- bucketName,
333
- region,
334
- progressCallback
335
- );
336
-
337
- const zipBuffer = await zipFiles(files, progressCallback);
338
- await uploadToStorageBucket(
339
- context,
340
- bucket,
341
- zipBuffer,
342
- ZIP_FILE_NAME,
343
- progressCallback
344
- );
345
- await logAndProgress('Source code uploaded successfully', progressCallback);
346
-
347
- await ensureArtifactRegistryRepoExists(
348
- context,
349
- projectId,
350
- region,
351
- REPO_NAME,
352
- 'DOCKER',
353
- progressCallback
354
- );
489
+ // Determines if we can use the direct source deployment (no Cloud Build)
490
+ const { hasDockerfile, deploymentAttrs } =
491
+ makeFileDeploymentMetadata(files);
355
492
 
356
- const buildResult = await triggerCloudBuild(
357
- context,
358
- projectId,
359
- region,
360
- bucketName,
361
- ZIP_FILE_NAME,
362
- REPO_NAME,
363
- imageUrl,
364
- hasDockerfile,
365
- progressCallback
366
- );
493
+ await logAndProgress(`Dockerfile: ${hasDockerfile}`, progressCallback);
367
494
 
368
- const builtImageUrl = buildResult.results.images[0].name;
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
+ }
369
516
 
370
- const service = await deployToCloudRun(
517
+ return await deployWithBuild(
371
518
  context,
372
519
  projectId,
373
520
  region,
374
521
  serviceName,
375
- builtImageUrl,
522
+ files,
523
+ hasDockerfile,
524
+ bucketName,
376
525
  progressCallback,
377
526
  skipIamCheck
378
527
  );
379
-
380
- await logAndProgress(`Deployment Completed Successfully`, progressCallback);
381
- return service;
382
528
  } catch (error) {
383
529
  const deployFailedMessage = `Deployment Failed: ${error.message}`;
384
530
  console.error(`Deployment Failed`, error);
@@ -444,7 +590,7 @@ export async function deployImage({
444
590
  await ensureApisEnabled(
445
591
  context,
446
592
  projectId,
447
- REQUIRED_APIS_FOR_IMAGE_DEPLOY,
593
+ REQUIRED_APIS.IMAGE_DEPLOY,
448
594
  progressCallback
449
595
  );
450
596
 
@@ -460,10 +606,11 @@ export async function deployImage({
460
606
  serviceName,
461
607
  imageUrl,
462
608
  progressCallback,
463
- skipIamCheck
609
+ skipIamCheck,
610
+ DEPLOYMENT_TYPES.IMAGE
464
611
  );
465
612
 
466
- await logAndProgress(`Deployment Completed Successfully`, progressCallback);
613
+ await logAndProgress(`Deployment completed successfully`, progressCallback);
467
614
  return service;
468
615
  } catch (error) {
469
616
  const deployFailedMessage = `Deployment Failed: ${error.message}`;
@@ -0,0 +1,194 @@
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 path from 'path';
18
+ import fs from 'fs';
19
+ import { DEPLOYMENT_CONFIG, RUNTIMES } from './constants.js';
20
+
21
+ /**
22
+ * Checks if the input is a single folder path.
23
+ * @param {Array<string|Object>} files - Array of file paths or file objects.
24
+ * @returns {boolean} - true if files contains exactly one string which is a directory.
25
+ */
26
+ function isFolder(files) {
27
+ if (files.length !== 1 || typeof files[0] !== 'string') return false;
28
+ try {
29
+ return fs.statSync(files[0]).isDirectory();
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Checks if a Dockerfile exists in the provided files array.
37
+ * @param {Array<string|Object>} files - An array of file paths or objects with filename properties.
38
+ * @returns {boolean} - Returns true if a Dockerfile is found, false otherwise.
39
+ */
40
+ function checkIfDockerFileExists(files) {
41
+ if (isFolder(files)) {
42
+ // Handle folder deployment: check for Dockerfile inside the folder
43
+ return (
44
+ fs.existsSync(path.join(files[0], 'Dockerfile')) ||
45
+ fs.existsSync(path.join(files[0], 'dockerfile'))
46
+ );
47
+ }
48
+
49
+ // Handle file list deployment or file content deployment
50
+ for (const file of files) {
51
+ if (typeof file === 'string') {
52
+ if (path.basename(file).toLowerCase() === 'dockerfile') {
53
+ return true;
54
+ }
55
+ } else if (typeof file === 'object' && file.filename) {
56
+ if (path.basename(file.filename).toLowerCase() === 'dockerfile') {
57
+ return true;
58
+ }
59
+ }
60
+ }
61
+ return false;
62
+ }
63
+
64
+ /**
65
+ * Checks if the provided folder appears to be a Node.js project.
66
+ * It looks for a `package.json` file in the root of the first path in the `files` array.
67
+ * @param {string[]} files - Array of file paths
68
+ * @returns {boolean}
69
+ */
70
+ function checkIfNodeJsRuntime(files) {
71
+ const packageJsonPath = path.join(files[0], 'package.json');
72
+ return fs.existsSync(packageJsonPath);
73
+ }
74
+
75
+ /**
76
+ * Returns an empty deployment attributes object.
77
+ * This is used as a default or when attributes cannot be determined.
78
+ * @returns {{runtime: undefined, cmd: undefined, args: undefined, baseImage: undefined}}
79
+ */
80
+ function getEmptyDeploymentAttrs() {
81
+ return {
82
+ runtime: undefined,
83
+ cmd: undefined,
84
+ args: undefined,
85
+ baseImage: undefined,
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Extracts deployment attributes (runtime, command, args, baseImage) from a Node.js project's `package.json`.
91
+ * This function is used for direct source deployments, bypassing Cloud Build.
92
+ * It returns empty attributes if a `start` script is missing, invalid, or if `engines.node` is specified (indicating a need for Cloud Build).
93
+ * @param {string[]} files - Array of file paths
94
+ * @returns {{runtime: string|undefined, cmd: string[]|undefined, args: string[]|undefined, baseImage: string|undefined}} Deployment attributes object.
95
+ */
96
+ function getNodeJsDeploymentAttrs(files) {
97
+ const packageJsonPath = path.join(files[0], 'package.json');
98
+ let packageJson;
99
+
100
+ try {
101
+ packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
102
+ } catch (error) {
103
+ // If package.json exists but is invalid, we can't extract attributes.
104
+ return getEmptyDeploymentAttrs();
105
+ }
106
+
107
+ const startScript = packageJson?.scripts?.start;
108
+ const hasNodeEngine = packageJson?.engines?.node;
109
+
110
+ if (typeof startScript !== 'string' || !startScript.trim() || hasNodeEngine) {
111
+ return getEmptyDeploymentAttrs();
112
+ }
113
+
114
+ // Handle multiple spaces properly
115
+ const parts = startScript.trim().split(/\s+/);
116
+
117
+ return {
118
+ runtime: RUNTIMES.NODEJS,
119
+ cmd: [parts[0]],
120
+ args: parts.slice(1),
121
+ baseImage: DEPLOYMENT_CONFIG.DEFAULT_NODE_BASE_IMAGE, // latest default base image
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Determines deployment attributes based on the file list/folder content.
127
+ * @param {Array<string|Object>} files - Array of file paths or file objects.
128
+ * @returns {{runtime: string|undefined, cmd: string[]|undefined, args: string[]|undefined, baseImage: string|undefined}} Deployment attributes object.
129
+ */
130
+ function getDeploymentAttrs(files) {
131
+ // Currently only support detection of NodeJs runtime
132
+ if (!isFolder(files) || !checkIfNodeJsRuntime(files)) {
133
+ // TODO: support file list deployment runtime detection
134
+ return getEmptyDeploymentAttrs();
135
+ }
136
+
137
+ return getNodeJsDeploymentAttrs(files);
138
+ }
139
+
140
+ /**
141
+ * Creates metadata for the file deployment, including Dockerfile presence and runtime attributes.
142
+ * @param {Array<string|Object>} files - Array of file paths or file objects to be deployed.
143
+ * @returns {{hasDockerfile: boolean, deploymentAttrs: {runtime: string|undefined, cmd: string[]|undefined, args: string[]|undefined, baseImage: string|undefined}}} Metadata object.
144
+ */
145
+ export function makeFileDeploymentMetadata(files) {
146
+ return {
147
+ hasDockerfile: checkIfDockerFileExists(files),
148
+ deploymentAttrs: getDeploymentAttrs(files),
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Checks if a zip-based source deployment (no-build) is feasible.
154
+ * @param {object} metadata - The deployment metadata.
155
+ * @param {boolean} metadata.hasDockerfile - Whether a Dockerfile is present.
156
+ * @param {{runtime: string|undefined, cmd: string[]|undefined, args: string[]|undefined}} metadata.deploymentAttrs - Deployment attributes.
157
+ * @returns {boolean} True if zip deployment is feasible.
158
+ */
159
+ export function canDeployWithoutBuild({ hasDockerfile, deploymentAttrs }) {
160
+ return !!(
161
+ !hasDockerfile &&
162
+ deploymentAttrs &&
163
+ deploymentAttrs.runtime &&
164
+ deploymentAttrs.cmd &&
165
+ deploymentAttrs.args
166
+ );
167
+ }
168
+
169
+ /**
170
+ * Creates the container spec for a direct source deployment (no Cloud Build).
171
+ * @param {object} params - The parameters for creating the container spec.
172
+ * @param {string} params.bucketName - The GCS bucket name where the source is uploaded.
173
+ * @param {string} params.fileName - The GCS object name (e.g., source.tar.gz).
174
+ * @param {{cmd: string[], args: string[], baseImage: string}} params.deploymentAttrs - Deployment attributes including command, args, and the base image to use.
175
+ * @returns {object} The container specification object for the Cloud Run service.
176
+ */
177
+ export function createDirectSourceDeploymentContainer({
178
+ bucketName,
179
+ fileName,
180
+ deploymentAttrs,
181
+ }) {
182
+ return {
183
+ image: DEPLOYMENT_CONFIG.NO_BUILD_IMAGE_TYPE,
184
+ baseImageUri: deploymentAttrs.baseImage,
185
+ sourceCode: {
186
+ cloudStorageSource: {
187
+ bucket: bucketName,
188
+ object: fileName,
189
+ },
190
+ },
191
+ command: deploymentAttrs.cmd,
192
+ args: deploymentAttrs.args,
193
+ };
194
+ }