Skip to content

Commit 7c2668e

Browse files
committed
fix(deploy): treat any non-zero gcloud exit code as failure, add argv construction tests
spawnAsync's close handler only rejected on code === 1. gcloud's own docs only promise a non-zero exit on failure, and a killed process (e.g. an out-of-memory gcloud builds submit) reports code === null, both of which previously resolved as success, so a failed deploy could be reported as successful. Now rejects on any code !== 0. Also extracts the gcloud args construction for both cloud run calls (buildCloudRunBuildsSubmitArgs, buildCloudRunDeployArgs) into pure, exported functions, and adds tests asserting a value containing a space (region, firebaseProject, a cloudRunOptions value) stays a single argv entry rather than being split into extra flags, locking in the fix from the previous commit without needing to mock child_process.spawn.
1 parent 8859c46 commit 7c2668e

2 files changed

Lines changed: 74 additions & 19 deletions

File tree

src/schematics/deploy/actions.jasmine.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import { join } from 'path';
33
import { BuilderContext, BuilderRun, ScheduleOptions, Target } from '@angular-devkit/architect';
44
import { JsonObject, logging } from '@angular-devkit/core';
5-
import { BuildTarget, FSHost, FirebaseDeployConfig, FirebaseTools } from '../interfaces';
6-
import deploy, { deployToFunction } from './actions.js'
5+
import { BuildTarget, DeployBuilderSchema, FSHost, FirebaseDeployConfig, FirebaseTools } from '../interfaces';
6+
import deploy, { buildCloudRunBuildsSubmitArgs, buildCloudRunDeployArgs, deployToFunction } from './actions.js'
77
import 'jasmine';
88

99
let context: BuilderContext;
@@ -300,3 +300,44 @@ describe('universal deployment', () => {
300300
expect(spy).not.toHaveBeenCalled();
301301
});*/
302302
});
303+
304+
describe('Cloud Run gcloud argv construction', () => {
305+
// Regression coverage for the argv-injection fix: these options used to be interpolated
306+
// into a single command string and split on whitespace, so a value containing a space
307+
// would land as extra, unintended argv entries. They're now passed straight through as
308+
// individual array elements.
309+
const INJECTED_REGION = 'us-central1 --set-env-vars=INJECTED=owned';
310+
const INJECTED_PROJECT = `${FIREBASE_PROJECT} --format=json`;
311+
312+
it('keeps a region value containing a space as a single --region argument', () => {
313+
const options: DeployBuilderSchema = { firebaseProject: FIREBASE_PROJECT, region: INJECTED_REGION };
314+
const args = buildCloudRunDeployArgs('my-service', options, []);
315+
316+
expect(args[args.indexOf('--region') + 1]).toBe(INJECTED_REGION);
317+
expect(args).not.toContain('--set-env-vars=INJECTED=owned');
318+
});
319+
320+
it('keeps a firebaseProject value containing a space as a single --project argument (deploy)', () => {
321+
const options: DeployBuilderSchema = { firebaseProject: INJECTED_PROJECT, region: 'us-central1' };
322+
const args = buildCloudRunDeployArgs('my-service', options, []);
323+
324+
expect(args[args.indexOf('--project') + 1]).toBe(INJECTED_PROJECT);
325+
expect(args).not.toContain('--format=json');
326+
});
327+
328+
it('keeps a firebaseProject value containing a space as a single --project argument (builds submit)', () => {
329+
const options: DeployBuilderSchema = { firebaseProject: INJECTED_PROJECT };
330+
const args = buildCloudRunBuildsSubmitArgs('cloudRunOut', 'my-service', options);
331+
332+
expect(args[args.indexOf('--project') + 1]).toBe(INJECTED_PROJECT);
333+
expect(args).not.toContain('--format=json');
334+
});
335+
336+
it('passes cloudRunOptions through as their own argv entries', () => {
337+
const options: DeployBuilderSchema = { firebaseProject: FIREBASE_PROJECT, region: 'us-central1' };
338+
const args = buildCloudRunDeployArgs('my-service', options, ['--vpc-connector', 'my-connector --unset-env-vars=OWNED']);
339+
340+
expect(args[args.indexOf('--vpc-connector') + 1]).toBe('my-connector --unset-env-vars=OWNED');
341+
expect(args).not.toContain('--unset-env-vars=OWNED');
342+
});
343+
});

src/schematics/deploy/actions.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const spawnAsync = async (
5151
reject(error);
5252
});
5353
spawnProcess.on('close', (code) => {
54-
if (code === 1) {
54+
if (code !== 0) {
5555
reject(Buffer.concat(errorChunks).toString());
5656
return;
5757
}
@@ -279,6 +279,34 @@ export const deployToFunction = async (
279279
};
280280

281281

282+
// Exported (rather than kept private) so the argv shape can be asserted directly in tests,
283+
// without having to mock child_process.spawn.
284+
export const buildCloudRunBuildsSubmitArgs = (
285+
cloudRunOut: string,
286+
serviceId: string,
287+
options: DeployBuilderOptions
288+
): string[] => [
289+
'builds', 'submit', cloudRunOut,
290+
'--tag', `gcr.io/${options.firebaseProject}/${serviceId}`,
291+
'--project', options.firebaseProject,
292+
'--quiet',
293+
];
294+
295+
export const buildCloudRunDeployArgs = (
296+
serviceId: string,
297+
options: DeployBuilderOptions,
298+
deployArguments: string[]
299+
): string[] => [
300+
'run', 'deploy', serviceId,
301+
'--image', `gcr.io/${options.firebaseProject}/${serviceId}`,
302+
'--project', options.firebaseProject,
303+
...deployArguments,
304+
'--platform', 'managed',
305+
'--allow-unauthenticated',
306+
'--region', options.region,
307+
'--quiet',
308+
];
309+
282310
export const deployToCloudRun = async (
283311
firebaseTools: FirebaseTools,
284312
context: BuilderContext,
@@ -368,22 +396,8 @@ export const deployToCloudRun = async (
368396
if (cloudRunOptions.vpcConnector) { deployArguments.push('--vpc-connector', cloudRunOptions.vpcConnector); }
369397

370398
context.logger.info(`📦 Deploying to Cloud Run`);
371-
await spawnAsync('gcloud', [
372-
'builds', 'submit', cloudRunOut,
373-
'--tag', `gcr.io/${options.firebaseProject}/${serviceId}`,
374-
'--project', options.firebaseProject,
375-
'--quiet',
376-
]);
377-
await spawnAsync('gcloud', [
378-
'run', 'deploy', serviceId,
379-
'--image', `gcr.io/${options.firebaseProject}/${serviceId}`,
380-
'--project', options.firebaseProject,
381-
...deployArguments,
382-
'--platform', 'managed',
383-
'--allow-unauthenticated',
384-
'--region', options.region,
385-
'--quiet',
386-
]);
399+
await spawnAsync('gcloud', buildCloudRunBuildsSubmitArgs(cloudRunOut, serviceId, options));
400+
await spawnAsync('gcloud', buildCloudRunDeployArgs(serviceId, options, deployArguments));
387401

388402
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
389403
const siteTarget = options.target ?? context.target!.project;

0 commit comments

Comments
 (0)