diff --git a/CHANGELOG.md b/CHANGELOG.md index e25cd25abf6..a8c817691a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,2 @@ -- Added 'hosting' to the 'firebase_init' MCP tool. +- Added 'hosting' to the 'firebase_init' MCP tool (#9375) +- Revert logic to abort function deploys in non-interactive mode when secrets are missing. (#9378) diff --git a/src/deploy/functions/params.spec.ts b/src/deploy/functions/params.spec.ts index ca7ee49b792..efc87040dc9 100644 --- a/src/deploy/functions/params.spec.ts +++ b/src/deploy/functions/params.spec.ts @@ -3,6 +3,8 @@ import * as sinon from "sinon"; import * as prompt from "../../prompt"; import * as params from "./params"; +import * as secretManager from "../../gcp/secretManager"; +import { FirebaseError } from "../../error"; const expect = chai.expect; const fakeConfig = { @@ -298,4 +300,30 @@ describe("resolveParams", () => { input.resolves("22"); await expect(params.resolveParams(paramsToResolve, fakeConfig, {})).to.eventually.be.rejected; }); + + it("does not throw in non-interactive mode if secret exists in cloud", async () => { + const paramsToResolve: params.Param[] = [{ name: "MY_SECRET", type: "secret" }]; + const getSecretMetadataStub = sinon.stub(secretManager, "getSecretMetadata").resolves({ + secret: { name: "MY_SECRET", projectId: "foo", labels: {}, replication: {} }, + secretVersion: { versionId: "1", state: "ENABLED", secret: {} as any }, + }); + + await expect(params.resolveParams(paramsToResolve, fakeConfig, {}, true)).to.be.fulfilled; + + getSecretMetadataStub.restore(); + }); + + it("throws in non-interactive mode if secret is missing in cloud", async () => { + const paramsToResolve: params.Param[] = [{ name: "MY_SECRET", type: "secret" }]; + const getSecretMetadataStub = sinon.stub(secretManager, "getSecretMetadata").resolves({ + secret: undefined, + }); + + await expect(params.resolveParams(paramsToResolve, fakeConfig, {}, true)).to.be.rejectedWith( + FirebaseError, + /In non-interactive mode but have no value for the secret/, + ); + + getSecretMetadataStub.restore(); + }); }); diff --git a/src/deploy/functions/params.ts b/src/deploy/functions/params.ts index 31a4a80c3e8..69ebe85212c 100644 --- a/src/deploy/functions/params.ts +++ b/src/deploy/functions/params.ts @@ -395,26 +395,10 @@ export async function resolveParams( const [needSecret, needPrompt] = partition(outstanding, (param) => param.type === "secret"); - // Check for missing secrets in non-interactive mode - if (nonInteractive && needSecret.length > 0) { - const secretNames = needSecret.map((p) => p.name).join(", "); - const commands = needSecret - .map( - (p) => - `\tfirebase functions:secrets:set ${p.name}${(p as SecretParam).format === "json" ? " --format=json --data-file " : ""}`, - ) - .join("\n"); - throw new FirebaseError( - `In non-interactive mode but have no value for the following secrets: ${secretNames}\n\n` + - "Set these secrets before deploying:\n" + - commands, - ); - } - // The functions emulator will handle secrets if (!isEmulator) { for (const param of needSecret) { - await handleSecret(param as SecretParam, firebaseConfig.projectId); + await handleSecret(param as SecretParam, firebaseConfig.projectId, nonInteractive); } } @@ -481,9 +465,20 @@ function populateDefaultParams(config: FirebaseConfig): Record { +async function handleSecret( + secretParam: SecretParam, + projectId: string, + nonInteractive?: boolean, +): Promise { const metadata = await secretManager.getSecretMetadata(projectId, secretParam.name, "latest"); if (!metadata.secret) { + if (nonInteractive) { + throw new FirebaseError( + `In non-interactive mode but have no value for the secret: ${secretParam.name}\n\n` + + "Set this secret before deploying:\n" + + `\tfirebase functions:secrets:set ${secretParam.name}${secretParam.format === "json" ? " --format=json --data-file " : ""}`, + ); + } const promptMessage = `This secret will be stored in Cloud Secret Manager (https://cloud.google.com/secret-manager/pricing) as ${ secretParam.name }. Enter ${secretParam.format === "json" ? "a JSON value" : "a value"} for ${