Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces support for deploying Cloud Run services directly via the Firebase CLI under the 'direct_cloud_run' experiment, adding the 'run' deploy target, integration with 'firebase init', and comprehensive tests. The review feedback suggests several key improvements: handling expected user-facing errors by importing and throwing 'FirebaseError', avoiding non-null assertions on the main container template to prevent runtime errors, ensuring temporary local archives are cleaned up in a 'finally' block to avoid disk leaks, parallelizing service preparation using 'Promise.all' for better performance, and adding defensive checks for potentially undefined configuration properties.
| import * as artifactregistry from "../../gcp/artifactregistry"; | ||
| import * as runv2 from "../../gcp/runv2"; | ||
| import * as gcs from "../../gcp/storage"; | ||
| import { getProjectNumber } from "../../getProjectNumber"; |
There was a problem hiding this comment.
Import FirebaseError from ../../error to use it for throwing user-facing errors, adhering to the repository style guide.
| import { getProjectNumber } from "../../getProjectNumber"; | |
| import { FirebaseError } from "../../error"; | |
| import { getProjectNumber } from "../../getProjectNumber"; |
References
- Throw FirebaseError for expected, user-facing errors. (link)
| const template = svc.existing | ||
| ? copyTemplate(svc.existing) | ||
| : { containers: [{ name: serviceId, image: "" }] }; | ||
| const container = mainContainer(template)!; |
There was a problem hiding this comment.
Avoid using the non-null assertion operator !. If mainContainer(template) returns undefined, accessing container.image will throw a runtime error. Instead, add a defensive check and throw a FirebaseError.
const container = mainContainer(template);
if (!container) {
throw new FirebaseError(`No container found in the template for service ${serviceId}`);
}References
- Use strict null checks and handle undefined/null explicitly. (link)
| logLabeledBullet("run", `Uploading source for service ${serviceId}...`); | ||
| const { bucket, object } = await gcs.uploadObject( | ||
| { file: archive, stream: fs.createReadStream(archive) }, | ||
| bucketName, | ||
| gcs.ContentType.ZIP, | ||
| ); | ||
| return { bucket, object }; |
There was a problem hiding this comment.
The temporary archive file created by createSourceDeployArchive is uploaded to GCS but never deleted from the local disk. To prevent leaking disk space, ensure the temporary archive is cleaned up in a finally block after the upload completes.
| logLabeledBullet("run", `Uploading source for service ${serviceId}...`); | |
| const { bucket, object } = await gcs.uploadObject( | |
| { file: archive, stream: fs.createReadStream(archive) }, | |
| bucketName, | |
| gcs.ContentType.ZIP, | |
| ); | |
| return { bucket, object }; | |
| logLabeledBullet("run", `Uploading source for service ${serviceId}...`); | |
| try { | |
| const { bucket, object } = await gcs.uploadObject( | |
| { file: archive, stream: fs.createReadStream(archive) }, | |
| bucketName, | |
| gcs.ContentType.ZIP, | |
| ); | |
| return { bucket, object }; | |
| } finally { | |
| try { | |
| fs.rmSync(archive, { force: true }); | |
| } catch { | |
| // Ignore cleanup errors to avoid masking the original upload error | |
| } | |
| } |
| * Returns the Cloud Run services in firebase.json that match the --only filter. | ||
| */ | ||
| export function getServiceConfigs(options: Options): RunSingle[] { | ||
| const configs = [options.config.src.run || []].flat(); |
There was a problem hiding this comment.
Defensively handle cases where options.config or options.config.src might be undefined (e.g., in unit tests or custom environments) by using optional chaining.
| const configs = [options.config.src.run || []].flat(); | |
| const configs = [options.config?.src?.run || []].flat(); |
References
- Use strict null checks and handle undefined/null explicitly. (link)
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces support for deploying Cloud Run services directly from local source using the Firebase CLI. It adds the new run deploy target, implements its prepare, deploy, and release phases, integrates it into the initialization flow, and adds comprehensive unit and end-to-end tests. The review feedback highlights several key improvements: adhering to strict null checks by avoiding the non-null assertion operator on the main container, cleaning up temporary local archive files after upload to prevent disk space leaks, verifying the existence of the deployed service URI before logging, and using npx mocha in the test script for better environment robustness.
| import { Context, Payload, ServiceDeploy } from "./args"; | ||
| import { |
There was a problem hiding this comment.
To support explicit null/undefined handling and avoid using the non-null assertion operator, we need to import FirebaseError from ../../error.
| import { Context, Payload, ServiceDeploy } from "./args"; | |
| import { | |
| import { FirebaseError } from "../../error"; | |
| import { Context, Payload, ServiceDeploy } from "./args"; | |
| import { |
| const template = svc.existing | ||
| ? copyTemplate(svc.existing) | ||
| : { containers: [{ name: serviceId, image: "" }] }; | ||
| const container = mainContainer(template)!; |
There was a problem hiding this comment.
Using the non-null assertion operator ! violates the repository style guide rule to use strict null checks and handle undefined/null explicitly. Please check if the container is defined and throw a FirebaseError if it is not.
const container = mainContainer(template);
if (!container) {
throw new FirebaseError(`No container found in the template for service ${serviceId}`);
}References
- Use strict null checks and handle undefined/null explicitly. (link)
| const archive = await createSourceDeployArchive( | ||
| cfg, | ||
| path.join(options.config.projectDir, cfg.rootDir), | ||
| ); | ||
| logLabeledBullet("run", `Uploading source for service ${serviceId}...`); | ||
| const { bucket, object } = await gcs.uploadObject( | ||
| { file: archive, stream: fs.createReadStream(archive) }, | ||
| bucketName, | ||
| gcs.ContentType.ZIP, | ||
| ); | ||
| return { bucket, object }; |
There was a problem hiding this comment.
The temporary archive file created by createSourceDeployArchive is uploaded to GCS but never deleted from the local disk. To prevent leaking disk space in the temporary directory, wrap the upload in a try...finally block and clean up the archive file.
| const archive = await createSourceDeployArchive( | |
| cfg, | |
| path.join(options.config.projectDir, cfg.rootDir), | |
| ); | |
| logLabeledBullet("run", `Uploading source for service ${serviceId}...`); | |
| const { bucket, object } = await gcs.uploadObject( | |
| { file: archive, stream: fs.createReadStream(archive) }, | |
| bucketName, | |
| gcs.ContentType.ZIP, | |
| ); | |
| return { bucket, object }; | |
| const archive = await createSourceDeployArchive( | |
| cfg, | |
| path.join(options.config.projectDir, cfg.rootDir), | |
| ); | |
| logLabeledBullet("run", `Uploading source for service ${serviceId}...`); | |
| try { | |
| const { bucket, object } = await gcs.uploadObject( | |
| { file: archive, stream: fs.createReadStream(archive) }, | |
| bucketName, | |
| gcs.ContentType.ZIP, | |
| ); | |
| return { bucket, object }; | |
| } finally { | |
| try { | |
| fs.rmSync(archive, { force: true }); | |
| } catch (err) { | |
| // ignore | |
| } | |
| } |
| for (const { config, deployed } of payload.run?.services || []) { | ||
| logLabeledSuccess("run", `Deployed service ${config.serviceId} to ${deployed?.uri}`); | ||
| } |
There was a problem hiding this comment.
If deployed or deployed.uri is undefined, this will log Deployed service <id> to undefined. We should explicitly check that deployed?.uri is defined before logging the success message.
for (const { config, deployed } of payload.run?.services || []) {
if (deployed?.uri) {
logLabeledSuccess("run", `Deployed service ${config.serviceId} to ${deployed.uri}`);
}
}3ba264a to
1086ebe
Compare
Description
Scenarios Tested
Sample Commands