From 8859c46a85aec587f494ccc81683989f998f480b Mon Sep 17 00:00:00 2001 From: Herdiyan Adam Putra Date: Sat, 1 Aug 2026 01:51:04 +0700 Subject: [PATCH] fix(deploy): pass gcloud arguments as an array instead of a joined string spawnAsync built the gcloud command as a single template-literal string and split it on whitespace before handing it to spawn(). Any deploy option containing a space (region, firebaseProject, functionName, cloudRunOptions.vpcConnector, none of which have a schema pattern) would be split into extra argv entries, letting a value from angular.json add unintended flags to the gcloud builds submit / run deploy / auth activate-service-account invocations. spawnAsync now takes command and args separately, matching child_process spawn's own signature, and the three call sites build their argument lists as arrays instead of interpolating into one string. This removes the join/split round-trip entirely rather than trying to validate each field. --- src/schematics/deploy/actions.ts | 40 +++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/schematics/deploy/actions.ts b/src/schematics/deploy/actions.ts index 9a2fb7cde..b13492b2b 100644 --- a/src/schematics/deploy/actions.ts +++ b/src/schematics/deploy/actions.ts @@ -32,11 +32,11 @@ const DEFAULT_CLOUD_RUN_OPTIONS: Partial = { const spawnAsync = async ( command: string, + args: string[], options?: SpawnOptionsWithoutStdio ) => new Promise((resolve, reject) => { - const [spawnCommand, ...args] = command.split(/\s+/); - const spawnProcess = spawn(spawnCommand, args, options); + const spawnProcess = spawn(command, args, options); const chunks: Buffer[] = []; const errorChunks: Buffer[] = []; spawnProcess.stdout.on('data', (data) => { @@ -353,25 +353,37 @@ export const deployToCloudRun = async ( throw new SchematicsException('Cloud Run preview not supported.'); } - const deployArguments: any[] = []; + const deployArguments: string[] = []; const cloudRunOptions = options.cloudRunOptions || {}; Object.entries(DEFAULT_CLOUD_RUN_OPTIONS).forEach(([k, v]) => { cloudRunOptions[k] ||= v; }); // lean on the schema for validation (rather than sanitize) - if (cloudRunOptions.cpus) { deployArguments.push('--cpu', cloudRunOptions.cpus); } - if (cloudRunOptions.maxConcurrency) { deployArguments.push('--concurrency', cloudRunOptions.maxConcurrency); } - if (cloudRunOptions.maxInstances) { deployArguments.push('--max-instances', cloudRunOptions.maxInstances); } - if (cloudRunOptions.memory) { deployArguments.push('--memory', cloudRunOptions.memory); } - if (cloudRunOptions.minInstances) { deployArguments.push('--min-instances', cloudRunOptions.minInstances); } - if (cloudRunOptions.timeout) { deployArguments.push('--timeout', cloudRunOptions.timeout); } + if (cloudRunOptions.cpus) { deployArguments.push('--cpu', cloudRunOptions.cpus.toString()); } + if (cloudRunOptions.maxConcurrency) { deployArguments.push('--concurrency', cloudRunOptions.maxConcurrency.toString()); } + if (cloudRunOptions.maxInstances) { deployArguments.push('--max-instances', cloudRunOptions.maxInstances.toString()); } + if (cloudRunOptions.memory) { deployArguments.push('--memory', cloudRunOptions.memory.toString()); } + if (cloudRunOptions.minInstances) { deployArguments.push('--min-instances', cloudRunOptions.minInstances.toString()); } + if (cloudRunOptions.timeout) { deployArguments.push('--timeout', cloudRunOptions.timeout.toString()); } if (cloudRunOptions.vpcConnector) { deployArguments.push('--vpc-connector', cloudRunOptions.vpcConnector); } - // TODO validate serviceId, firebaseProject, and vpcConnector both to limit errors and opp for injection - context.logger.info(`📦 Deploying to Cloud Run`); - await spawnAsync(`gcloud builds submit ${cloudRunOut} --tag gcr.io/${options.firebaseProject}/${serviceId} --project ${options.firebaseProject} --quiet`); - await spawnAsync(`gcloud run deploy ${serviceId} --image gcr.io/${options.firebaseProject}/${serviceId} --project ${options.firebaseProject} ${deployArguments.join(' ')} --platform managed --allow-unauthenticated --region=${options.region} --quiet`); + await spawnAsync('gcloud', [ + 'builds', 'submit', cloudRunOut, + '--tag', `gcr.io/${options.firebaseProject}/${serviceId}`, + '--project', options.firebaseProject, + '--quiet', + ]); + await spawnAsync('gcloud', [ + 'run', 'deploy', serviceId, + '--image', `gcr.io/${options.firebaseProject}/${serviceId}`, + '--project', options.firebaseProject, + ...deployArguments, + '--platform', 'managed', + '--allow-unauthenticated', + '--region', options.region, + '--quiet', + ]); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const siteTarget = options.target ?? context.target!.project; @@ -405,7 +417,7 @@ export default async function deploy( } if (!firebaseToken && process.env.GOOGLE_APPLICATION_CREDENTIALS) { - await spawnAsync(`gcloud auth activate-service-account --key-file ${process.env.GOOGLE_APPLICATION_CREDENTIALS}`); + await spawnAsync('gcloud', ['auth', 'activate-service-account', '--key-file', process.env.GOOGLE_APPLICATION_CREDENTIALS as string]); console.log(`Using Google Application Credentials.`); }