diff --git a/action.yml b/action.yml index 52bff61b..9ed35186 100644 --- a/action.yml +++ b/action.yml @@ -33,25 +33,16 @@ inputs: default: "7d" required: false projectId: - description: - "The project to deploy to. If you leave this blank, make sure you check in - a .firebaserc file" + description: "The project to deploy to. If you leave this blank, make sure you check in a .firebaserc file" required: false channelId: - description: "The ID of the channel to deploy to. If you leave this blank, - a preview channel and its ID will be auto-generated per branch or PR." + description: "The ID of the channel to deploy to. If you leave this blank, a preview channel and its ID will be auto-generated per branch or PR." required: false target: - description: - "The target name of the Hosting site to deploy to. If you leave this blank, - the default target or all targets defined in the .firebaserc will be deployed to. - Refer to the Hosting docs about [multiple sites](https://firebase.google.com/docs/hosting/multisites) - for more information about deploy targets." + description: "The target name of the Hosting site to deploy to. If you leave this blank, the default target or all targets defined in the .firebaserc will be deployed to. Refer to the Hosting docs about [multiple sites](https://firebase.google.com/docs/hosting/multisites) for more information about deploy targets." required: false entryPoint: - description: - "The location of your firebase.json file, relative to the root of your - directory" + description: "The location of your firebase.json file, relative to the root of your directory" default: "." required: false firebaseToolsVersion: @@ -64,6 +55,10 @@ inputs: Disable auto-commenting with the preview channel URL to the pull request default: "false" required: false + force: + description: "Pass 'true' to use the --force flag with firebase deploy. This will automatically accept any warning or prompts during deploy. Use with caution." + default: "false" + required: false outputs: urls: description: The url(s) deployed to diff --git a/bin/action.min.js b/bin/action.min.js index 9c931a29..7291ada8 100644 --- a/bin/action.min.js +++ b/bin/action.min.js @@ -92946,8 +92946,9 @@ async function execWithCredentials(args, projectId, gacFilename, opts) { let deployOutputBuf = []; const debug = opts.debug || false; const firebaseToolsVersion = opts.firebaseToolsVersion || "latest"; + const force = opts.force; try { - await exec_1.exec(`npx firebase-tools@${firebaseToolsVersion}`, [...args, ...(projectId ? ["--project", projectId] : []), debug ? "--debug" // gives a more thorough error message + await exec_1.exec(`npx firebase-tools@${firebaseToolsVersion}`, [...args, ...(projectId ? ["--project", projectId] : []), ...(force ? ["--force"] : []), debug ? "--debug" // gives a more thorough error message : "--json" // allows us to easily parse the output ], { listeners: { @@ -92968,7 +92969,8 @@ async function execWithCredentials(args, projectId, gacFilename, opts) { console.log("Retrying deploy with the --debug flag for better error output"); await execWithCredentials(args, projectId, gacFilename, { debug: true, - firebaseToolsVersion + firebaseToolsVersion, + force }); } else { throw e; @@ -92982,10 +92984,12 @@ async function deployPreview(gacFilename, deployConfig) { channelId, target, expires, - firebaseToolsVersion + firebaseToolsVersion, + force } = deployConfig; const deploymentText = await execWithCredentials(["hosting:channel:deploy", channelId, ...(target ? ["--only", target] : []), ...(expires ? ["--expires", expires] : [])], projectId, gacFilename, { - firebaseToolsVersion + firebaseToolsVersion, + force }); const deploymentResult = JSON.parse(deploymentText.trim()); return deploymentResult; @@ -92994,10 +92998,12 @@ async function deployProductionSite(gacFilename, productionDeployConfig) { const { projectId, target, - firebaseToolsVersion + firebaseToolsVersion, + force } = productionDeployConfig; const deploymentText = await execWithCredentials(["deploy", "--only", `hosting${target ? ":" + target : ""}`], projectId, gacFilename, { - firebaseToolsVersion + firebaseToolsVersion, + force }); const deploymentResult = JSON.parse(deploymentText); return deploymentResult; @@ -93181,6 +93187,7 @@ const entryPoint = core.getInput("entryPoint"); const target = core.getInput("target"); const firebaseToolsVersion = core.getInput("firebaseToolsVersion"); const disableComment = core.getInput("disableComment"); +const force = core.getInput("force") === "true"; async function run() { const isPullRequest = !!github.context.payload.pull_request; let finish = details => console.log(details); @@ -93212,7 +93219,8 @@ async function run() { const deployment = await deployProductionSite(gacFilename, { projectId, target, - firebaseToolsVersion + firebaseToolsVersion, + force }); if (deployment.status === "error") { throw Error(deployment.error); @@ -93237,7 +93245,8 @@ async function run() { expires, channelId, target, - firebaseToolsVersion + firebaseToolsVersion, + force }); if (deployment.status === "error") { throw Error(deployment.error); diff --git a/src/deploy.ts b/src/deploy.ts index 76c61858..f7a419b7 100644 --- a/src/deploy.ts +++ b/src/deploy.ts @@ -45,14 +45,18 @@ type DeployConfig = { target?: string; // Optional version specification for firebase-tools. Defaults to `latest`. firebaseToolsVersion?: string; + force?: boolean; }; export type ChannelDeployConfig = DeployConfig & { expires: string; channelId: string; + force?: boolean; }; -export type ProductionDeployConfig = DeployConfig & {}; +export type ProductionDeployConfig = DeployConfig & { + force?: boolean; +}; export function interpretChannelDeployResult( deployResult: ChannelSuccessResult @@ -74,11 +78,12 @@ async function execWithCredentials( args: string[], projectId, gacFilename, - opts: { debug?: boolean; firebaseToolsVersion?: string } + opts: { debug?: boolean; firebaseToolsVersion?: string; force?: boolean } ) { let deployOutputBuf: Buffer[] = []; const debug = opts.debug || false; const firebaseToolsVersion = opts.firebaseToolsVersion || "latest"; + const force = opts.force; try { await exec( @@ -86,6 +91,7 @@ async function execWithCredentials( [ ...args, ...(projectId ? ["--project", projectId] : []), + ...(force ? ["--force"] : []), debug ? "--debug" // gives a more thorough error message : "--json", // allows us to easily parse the output @@ -114,6 +120,7 @@ async function execWithCredentials( await execWithCredentials(args, projectId, gacFilename, { debug: true, firebaseToolsVersion, + force, }); } else { throw e; @@ -129,7 +136,7 @@ export async function deployPreview( gacFilename: string, deployConfig: ChannelDeployConfig ) { - const { projectId, channelId, target, expires, firebaseToolsVersion } = + const { projectId, channelId, target, expires, firebaseToolsVersion, force } = deployConfig; const deploymentText = await execWithCredentials( @@ -141,7 +148,7 @@ export async function deployPreview( ], projectId, gacFilename, - { firebaseToolsVersion } + { firebaseToolsVersion, force } ); const deploymentResult = JSON.parse(deploymentText.trim()) as @@ -155,13 +162,14 @@ export async function deployProductionSite( gacFilename, productionDeployConfig: ProductionDeployConfig ) { - const { projectId, target, firebaseToolsVersion } = productionDeployConfig; + const { projectId, target, firebaseToolsVersion, force } = + productionDeployConfig; const deploymentText = await execWithCredentials( ["deploy", "--only", `hosting${target ? ":" + target : ""}`], projectId, gacFilename, - { firebaseToolsVersion } + { firebaseToolsVersion, force } ); const deploymentResult = JSON.parse(deploymentText) as diff --git a/src/index.ts b/src/index.ts index fe34502b..1b03d77d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,6 +51,7 @@ const entryPoint = getInput("entryPoint"); const target = getInput("target"); const firebaseToolsVersion = getInput("firebaseToolsVersion"); const disableComment = getInput("disableComment"); +const force = getInput("force") === "true"; async function run() { const isPullRequest = !!context.payload.pull_request; @@ -94,6 +95,7 @@ async function run() { projectId, target, firebaseToolsVersion, + force, }); if (deployment.status === "error") { throw Error((deployment as ErrorResult).error); @@ -122,6 +124,7 @@ async function run() { channelId, target, firebaseToolsVersion, + force, }); if (deployment.status === "error") { diff --git a/test/deploy.test.ts b/test/deploy.test.ts index c72dd153..43f09d50 100644 --- a/test/deploy.test.ts +++ b/test/deploy.test.ts @@ -25,6 +25,18 @@ const baseLiveDeployConfig: ProductionDeployConfig = { projectId: "my-project", }; +const forceProductionDeployConfig: ProductionDeployConfig = { + projectId: "my-project", + force: true, +}; + +const forcePreviewDeployConfig: ChannelDeployConfig = { + projectId: "my-project", + channelId: "my-channel", + expires: undefined, + force: true, +}; + async function fakeExecFail( mainCommand: string, args: string[], @@ -128,6 +140,48 @@ describe("deploy", () => { }); }); + describe("deploy to preview channel with force flag", () => { + it("calls exec and interprets the output, including the --force flag when force is true", async () => { + // @ts-ignore read-only property + exec.exec = jest.fn(fakeExec); + + const deployOutput: ChannelSuccessResult = (await deployPreview( + "my-file", + forcePreviewDeployConfig + )) as ChannelSuccessResult; + + expect(exec.exec).toBeCalled(); + expect(deployOutput).toEqual(channelSingleSiteSuccess); + + // Check the arguments that exec was called with + // @ts-ignore Jest adds a magic "mock" property + const args = exec.exec.mock.calls; + const deployFlags = args[0][1]; + expect(deployFlags).toContain("hosting:channel:deploy"); + expect(deployFlags).toContain("--force"); + }); + + it("specifies a target when one is provided", async () => { + // @ts-ignore read-only property + exec.exec = jest.fn(fakeExec); + + const config: ChannelDeployConfig = { + ...forcePreviewDeployConfig, + target: "my-second-site", + }; + + await deployPreview("my-file", config); + + // Check the arguments that exec was called with + // @ts-ignore Jest adds a magic "mock" property + const args = exec.exec.mock.calls; + const deployFlags = args[0][1]; + expect(deployFlags).toContain("--only"); + expect(deployFlags).toContain("my-second-site"); + expect(deployFlags).toContain("--force"); + }); + }); + describe("deploy to live channel", () => { it("calls exec and interprets the output", async () => { // @ts-ignore read-only property @@ -150,4 +204,29 @@ describe("deploy", () => { expect(deployFlags).toContain("hosting"); }); }); + + describe("deploy to live channel with force flag", () => { + it("includes --force flag when force is true for deploy", async () => { + // @ts-ignore read-only property + exec.exec = jest.fn(fakeExec); + + const forceDeployOutput: ProductionSuccessResult = + (await deployProductionSite( + "my-file", + forceProductionDeployConfig + )) as ProductionSuccessResult; + + expect(exec.exec).toBeCalled(); + expect(forceDeployOutput).toEqual(liveDeploySingleSiteSuccess); + + // Check the arguments that exec was called with + // @ts-ignore Jest adds a magic "mock" property + const args = exec.exec.mock.calls; + const deployFlags = args[0][1]; + expect(deployFlags).toContain("deploy"); + expect(deployFlags).toContain("--only"); + expect(deployFlags).toContain("hosting"); + expect(deployFlags).toContain("--force"); + }); + }); });